Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.48% covered (success)
97.48%
1237 / 1269
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%
1094 / 1094
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_table
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
210
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']['total_time'] = [
442    'title' => t('Total Time'),
443    'help' => t('The total time of the visit.'),
444    'field' => [
445      'id' => 'visitors_number_range',
446    ],
447    'sort' => [
448      'id' => 'visitors_number_range',
449    ],
450    'filter' => [
451      'id' => 'numeric',
452    ],
453    'argument' => [
454      'id' => 'numeric',
455    ],
456  ];
457  $data['visitors_visit']['entry'] = [
458    'title' => t('Entry page'),
459    'help' => t('The first page viewed in the visit.'),
460    'field' => [
461      'id' => 'numeric',
462    ],
463    'relationship' => [
464      'title' => t('Entry page'),
465      'help' => t('Page view Log.'),
466      'base' => 'visitors_event',
467      'base field' => 'id',
468      'id' => 'standard',
469    ],
470    'sort' => [
471      'id' => 'standard',
472    ],
473    'filter' => [
474      'id' => 'numeric',
475    ],
476    'argument' => [
477      'id' => 'numeric',
478    ],
479  ];
480  $data['visitors_visit']['entry_time'] = [
481    'title' => t('Entry Time'),
482    'help' => t('The timestamp of the page view.'),
483    'field' => [
484      'id' => 'date',
485      'click sortable' => TRUE,
486    ],
487  ];
488  $data['visitors_visit']['exit'] = [
489    'title' => t('Exit page'),
490    'help' => t('The last page viewed in the visit.'),
491    'field' => [
492      'id' => 'numeric',
493    ],
494    'relationship' => [
495      'title' => t('Exit page'),
496      'help' => t('Exit Page view.'),
497      'base' => 'visitors_event',
498      'base field' => 'id',
499      'id' => 'standard',
500    ],
501    'sort' => [
502      'id' => 'standard',
503    ],
504    'filter' => [
505      'id' => 'numeric',
506    ],
507    'argument' => [
508      'id' => 'numeric',
509    ],
510  ];
511  $data['visitors_visit']['exit_time'] = [
512    'title' => t('Exit Time'),
513    'help' => t('The timestamp of the page view.'),
514    'field' => [
515      'id' => 'date',
516      'click sortable' => TRUE,
517    ],
518  ];
519  $data['visitors_visit']['time_since_first'] = [
520    'title' => t('Time Since First Visit'),
521    'help' => t("Seconds since the visitor's first visit."),
522    'field' => [
523      'id' => 'visitors_number_range',
524      'click sortable' => TRUE,
525    ],
526    'sort' => [
527      'id' => 'visitors_number_range',
528    ],
529    'filter' => [
530      'id' => 'standard',
531    ],
532    'argument' => [
533      'id' => 'numeric',
534    ],
535  ];
536  $data['visitors_visit']['time_since_last'] = [
537    'title' => t('Time Since Last Visit'),
538    'help' => t("Seconds since the visitor's last visit."),
539    'field' => [
540      'id' => 'visitors_number_range',
541      'click sortable' => TRUE,
542    ],
543    'sort' => [
544      'id' => 'visitors_number_range',
545    ],
546    'filter' => [
547      'id' => 'standard',
548    ],
549    'argument' => [
550      'id' => 'numeric',
551    ],
552  ];
553  $data['visitors_visit']['visit_time'] = [
554    'title' => t('Visit Time'),
555    'help' => t('Visit time filter.'),
556    'filter' => [
557      'id' => 'visitors_visit_date',
558      'field' => 'exit_time',
559    ],
560  ];
561  $data['visitors_visit']['config_id'] = [
562    'title' => t('Config Id'),
563    'help' => t('Visitor config hash.'),
564    'field' => [
565      'id' => 'standard',
566    ],
567    'filter' => [
568      'id' => 'string',
569    ],
570    'sort' => [
571      'id' => 'standard',
572    ],
573    'argument' => [
574      'id' => 'string',
575    ],
576  ];
577  $data['visitors_visit']['config_resolution'] = [
578    'title' => t('Resolution'),
579    'help' => t("The visitor's screen resolution."),
580    'field' => [
581      'id' => 'standard',
582    ],
583    'filter' => [
584      'id' => 'string',
585    ],
586    'argument' => [
587      'id' => 'string',
588    ],
589  ];
590  $data['visitors_visit']['config_pdf'] = [
591    'title' => t('PDF Plugin'),
592    'help' => t("The visitor's browser supports PDFs."),
593    'field' => [
594      'id' => 'visitors_pdf',
595    ],
596    'filter' => [
597      'id' => 'boolean',
598    ],
599  ];
600  $data['visitors_visit']['config_flash'] = [
601    'title' => t('Flash Plugin'),
602    'help' => t("The visitor's browser supports Flash."),
603    'field' => [
604      'id' => 'visitors_flash',
605    ],
606    'filter' => [
607      'id' => 'boolean',
608    ],
609  ];
610  $data['visitors_visit']['config_java'] = [
611    'title' => t('Java Plugin'),
612    'help' => t("The visitor's browser supports Java."),
613    'field' => [
614      'id' => 'visitors_java',
615    ],
616    'filter' => [
617      'id' => 'boolean',
618    ],
619  ];
620  $data['visitors_visit']['config_quicktime'] = [
621    'title' => t('Quicktime Plugin'),
622    'help' => t("The visitor's browser supports Quicktime."),
623    'field' => [
624      'id' => 'visitors_quicktime',
625    ],
626    'filter' => [
627      'id' => 'boolean',
628    ],
629  ];
630  $data['visitors_visit']['config_realplayer'] = [
631    'title' => t('Realplayer Plugin'),
632    'help' => t("The visitor's browser supports Realplayer."),
633    'field' => [
634      'id' => 'visitors_realplayer',
635    ],
636    'filter' => [
637      'id' => 'boolean',
638    ],
639  ];
640  $data['visitors_visit']['config_windowsmedia'] = [
641    'title' => t('Windows Media Plugin'),
642    'help' => t("The visitor's browser supports Windows Media."),
643    'field' => [
644      'id' => 'visitors_windowsmedia',
645    ],
646    'filter' => [
647      'id' => 'boolean',
648    ],
649  ];
650  $data['visitors_visit']['config_silverlight'] = [
651    'title' => t('Silverlight Plugin'),
652    'help' => t("The visitor's browser supports Silverlight."),
653    'field' => [
654      'id' => 'visitors_silverlight',
655    ],
656    'filter' => [
657      'id' => 'boolean',
658    ],
659  ];
660  $data['visitors_visit']['config_cookie'] = [
661    'title' => t('Cookie Plugin'),
662    'help' => t("The visitor's browser supports cookies."),
663    'field' => [
664      'id' => 'visitors_cookie',
665    ],
666    'filter' => [
667      'id' => 'boolean',
668    ],
669  ];
670  $data['visitors_visit']['config_browser_engine'] = [
671    'title' => t('Browser Engine'),
672    'help' => t('The engine used by the browser.'),
673    'field' => [
674      'id' => 'standard',
675    ],
676    'filter' => [
677      'id' => 'string',
678    ],
679    'argument' => [
680      'id' => 'string',
681    ],
682  ];
683  $data['visitors_visit']['config_browser_name'] = [
684    'title' => t('Browser Name'),
685    'help' => t('The name of the browser.'),
686    'field' => [
687      'id' => 'visitors_browser',
688    ],
689    'filter' => [
690      'id' => 'string',
691    ],
692    'argument' => [
693      'id' => 'string',
694    ],
695  ];
696  $data['visitors_visit']['config_browser_version'] = [
697    'title' => t('Browser Version'),
698    'help' => t('The version of the browser.'),
699    'field' => [
700      'id' => 'standard',
701    ],
702    'filter' => [
703      'id' => 'string',
704    ],
705    'argument' => [
706      'id' => 'string',
707    ],
708  ];
709  $data['visitors_visit']['config_client_type'] = [
710    'title' => t('Client type'),
711    'help' => t('The type of the client.'),
712    'field' => [
713      'id' => 'standard',
714    ],
715    'filter' => [
716      'id' => 'string',
717    ],
718    'argument' => [
719      'id' => 'string',
720    ],
721  ];
722  $data['visitors_visit']['config_device_brand'] = [
723    'title' => t('Device brand'),
724    'help' => t('The brand of the device.'),
725    'field' => [
726      'id' => 'visitors_brand',
727    ],
728    'filter' => [
729      'id' => 'string',
730    ],
731    'argument' => [
732      'id' => 'string',
733    ],
734  ];
735  $data['visitors_visit']['config_device_model'] = [
736    'title' => t('Device model'),
737    'help' => t('The model of the device.'),
738    'field' => [
739      'id' => 'standard',
740    ],
741    'filter' => [
742      'id' => 'string',
743    ],
744    'argument' => [
745      'id' => 'string',
746    ],
747  ];
748  $data['visitors_visit']['config_device_type'] = [
749    'title' => t('Device type'),
750    'help' => t('The type of device.'),
751    'field' => [
752      'id' => 'visitors_device',
753    ],
754    'filter' => [
755      'id' => 'string',
756    ],
757    'argument' => [
758      'id' => 'string',
759    ],
760  ];
761  $data['visitors_visit']['config_os'] = [
762    'title' => t('Operating System'),
763    'help' => t('The operating system.'),
764    'field' => [
765      'id' => 'visitors_operating_system',
766    ],
767    'filter' => [
768      'id' => 'string',
769    ],
770    'argument' => [
771      'id' => 'string',
772    ],
773  ];
774  $data['visitors_visit']['config_os_version'] = [
775    'title' => t('OS version'),
776    'help' => t('The version of the Operating System.'),
777    'field' => [
778      'id' => 'standard',
779    ],
780    'filter' => [
781      'id' => 'string',
782    ],
783    'argument' => [
784      'id' => 'string',
785    ],
786  ];
787  $data['visitors_visit']['bot'] = [
788    'title' => t('Bot'),
789    'help' => t("The visit is from a bot."),
790    'field' => [
791      'id' => 'boolean',
792    ],
793    'filter' => [
794      'id' => 'boolean',
795    ],
796    'argument' => [
797      'id' => 'numeric',
798    ],
799  ];
800  $data['visitors_visit']['location_browser_lang'] = [
801    'title' => t('Language'),
802    'help' => t('The browser language.'),
803    'field' => [
804      'id' => 'visitors_language',
805    ],
806    'filter' => [
807      'id' => 'string',
808    ],
809    'argument' => [
810      'id' => 'string',
811    ],
812  ];
813  $data['visitors_visit']['location_ip'] = [
814    'title' => t('Visitors IP'),
815    'help' => t('The IP of the visitors entry.'),
816    'field' => [
817      'id' => 'standard',
818    ],
819    'filter' => [
820      'id' => 'string',
821    ],
822    'argument' => [
823      'id' => 'string',
824    ],
825  ];
826  $data['visitors_visit']['location_continent'] = [
827    'title' => t('Continent'),
828    'help' => t('The location continent.'),
829    'field' => [
830      'id' => 'visitors_continent',
831    ],
832    'filter' => [
833      'id' => 'string',
834    ],
835    'argument' => [
836      'id' => 'string',
837    ],
838  ];
839  $data['visitors_visit']['location_country'] = [
840    'title' => t('Country'),
841    'help' => t('The location country.'),
842    'field' => [
843      'id' => 'visitors_country',
844    ],
845    'filter' => [
846      'id' => 'string',
847    ],
848    'argument' => [
849      'id' => 'string',
850    ],
851  ];
852  $data['visitors_visit']['location_region'] = [
853    'title' => t('Region'),
854    'help' => t('The region of the visitor.'),
855    'field' => [
856      'id' => 'standard',
857    ],
858    'filter' => [
859      'id' => 'string',
860    ],
861    'argument' => [
862      'id' => 'string',
863    ],
864  ];
865  $data['visitors_visit']['location_city'] = [
866    'title' => t('City'),
867    'help' => t('The city of the visitor.'),
868    'field' => [
869      'id' => 'standard',
870    ],
871    'filter' => [
872      'id' => 'string',
873    ],
874    'argument' => [
875      'id' => 'string',
876    ],
877  ];
878  $data['visitors_visit']['location_latitude'] = [
879    'title' => t('Location Latitude'),
880    'help' => t('The latitude derived from the IP address.'),
881    'field' => [
882      'id' => 'numeric',
883    ],
884    'filter' => [
885      'id' => 'numeric',
886    ],
887    'sort' => [
888      'id' => 'numeric',
889    ],
890  ];
891  $data['visitors_visit']['location_longitude'] = [
892    'title' => t('Location Longitude'),
893    'help' => t('The longitude derived from the IP address.'),
894    'field' => [
895      'id' => 'numeric',
896    ],
897    'filter' => [
898      'id' => 'numeric',
899    ],
900    'sort' => [
901      'id' => 'numeric',
902    ],
903  ];
904  $data['visitors_visit']['referer_type'] = [
905    'title' => t('Referer Type'),
906    'help' => t('The type of the referer.'),
907    'field' => [
908      'id' => 'standard',
909    ],
910    'filter' => [
911      'id' => 'string',
912    ],
913  ];
914  $data['visitors_visit']['referer_name'] = [
915    'title' => t('Referer Name'),
916    'help' => t('The name of the referer.'),
917    'field' => [
918      'id' => 'standard',
919    ],
920  ];
921  $data['visitors_visit']['referer_url'] = [
922    'title' => t('Referer URL'),
923    'help' => t('The URL of the referer.'),
924    'field' => [
925      'id' => 'standard',
926    ],
927  ];
928  $data['visitors_visit']['referer_keyword'] = [
929    'title' => t('Referer Keyword'),
930    'help' => t('The keyword of the referer.'),
931    'field' => [
932      'id' => 'standard',
933    ],
934  ];
935
936  $data['visitors_event']['table']['group'] = t('Visitors');
937  $data['visitors_event']['table']['base'] = [
938    'title' => t('Visitors Log'),
939    'help' => t('Visitors data from visitors DB table.'),
940  ];
941
942  $data['visitors_event']['id'] = [
943    'title' => t('Log Id'),
944    'help' => t('Visitors Log Id.'),
945    'field' => [
946      'id' => 'numeric',
947    ],
948    'sort' => [
949      'id' => 'standard',
950    ],
951    'filter' => [
952      'id' => 'numeric',
953    ],
954    'argument' => [
955      'id' => 'numeric',
956    ],
957  ];
958  $data['visitors_event']['page_view'] = [
959    'title' => t('Event Page View'),
960    'help' => t('Visitors Event Page View.'),
961    'field' => [
962      'id' => 'standard',
963    ],
964    'sort' => [
965      'id' => 'string',
966    ],
967    'filter' => [
968      'id' => 'string',
969    ],
970    'argument' => [
971      'id' => 'string',
972    ],
973  ];
974  $data['visitors_event']['visit_id'] = [
975    'title' => t('Event Id'),
976    'help' => t('Visitors Event Id.'),
977    'field' => [
978      'id' => 'numeric',
979    ],
980    'relationship' => [
981      'title' => t('Visitors Visit'),
982      'help' => t('Visit of the Log.'),
983      'base' => 'visitors_visit',
984      'base field' => 'id',
985      'id' => 'standard',
986    ],
987    'sort' => [
988      'id' => 'standard',
989    ],
990    'filter' => [
991      'id' => 'numeric',
992    ],
993    'argument' => [
994      'id' => 'numeric',
995    ],
996  ];
997  $data['visitors_event']['title'] = [
998    'title' => t('Visitors title'),
999    'help' => t('The title of the visitors entry.'),
1000    'field' => [
1001      'id' => 'standard',
1002    ],
1003    'filter' => [
1004      'id' => 'string',
1005    ],
1006  ];
1007  $data['visitors_event']['uid'] = [
1008    'title' => t('Visitors Event User'),
1009    'help' => t('The user ID of the event.'),
1010    'field' => [
1011      'id' => 'standard',
1012    ],
1013    'relationship' => [
1014      'title' => t('User'),
1015      'help' => t('The user entity from the visitor entry.'),
1016      'base' => 'users_field_data',
1017      'base field' => 'uid',
1018      'id' => 'standard',
1019    ],
1020    'filter' => [
1021      'id' => 'numeric',
1022    ],
1023    'argument' => [
1024      'id' => 'numeric',
1025    ],
1026  ];
1027  $data['visitors_event']['url_prefix'] = [
1028    'title' => t('URL Prefix'),
1029    'help' => t('The URL Prefix.'),
1030    'field' => [
1031      'id' => 'visitors_url_prefix',
1032    ],
1033  ];
1034  $data['visitors_event']['url'] = [
1035    'title' => t('Visitors URL'),
1036    'help' => t('The URL of the visitors entry.'),
1037    'field' => [
1038      'id' => 'standard',
1039    ],
1040    'filter' => [
1041      'id' => 'string',
1042    ],
1043  ];
1044
1045  $data['visitors_event']['path'] = [
1046    'title' => t('Visitors path'),
1047    'help' => t('The path of the visitors entry.'),
1048    'field' => [
1049      'id' => 'standard',
1050    ],
1051    'filter' => [
1052      'id' => 'string',
1053    ],
1054    'argument' => [
1055      'id' => 'string',
1056    ],
1057  ];
1058  $data['visitors_event']['route'] = [
1059    'title' => t('Route'),
1060    'help' => t('The route of the visitors entry.'),
1061    'field' => [
1062      'id' => 'standard',
1063    ],
1064    'filter' => [
1065      'id' => 'string',
1066    ],
1067    'argument' => [
1068      'id' => 'string',
1069    ],
1070  ];
1071  $data['visitors_event']['referrer_url'] = [
1072    'title' => t('Visitors referer'),
1073    'help' => t('The referer of the visitors entry.'),
1074    'field' => [
1075      'id' => 'standard',
1076    ],
1077    'filter' => [
1078      'id' => 'string',
1079    ],
1080  ];
1081  $data['visitors_event']['server'] = [
1082    'title' => t('Server'),
1083    'help' => t('The server that generated the response.'),
1084    'field' => [
1085      'id' => 'standard',
1086    ],
1087    'filter' => [
1088      'id' => 'string',
1089    ],
1090    'argument' => [
1091      'id' => 'string',
1092    ],
1093  ];
1094  $data['visitors_event']['pf_network'] = [
1095    'title' => t('Network'),
1096    'help' => t('Network performance.'),
1097    'field' => [
1098      'id' => 'numeric',
1099    ],
1100    'sort' => [
1101      'id' => 'standard',
1102    ],
1103    'filter' => [
1104      'id' => 'numeric',
1105    ],
1106    'argument' => [
1107      'id' => 'numeric',
1108    ],
1109  ];
1110  $data['visitors_event']['pf_server'] = [
1111    'title' => t('Server'),
1112    'help' => t('Server performance.'),
1113    'field' => [
1114      'id' => 'numeric',
1115    ],
1116    'sort' => [
1117      'id' => 'standard',
1118    ],
1119    'filter' => [
1120      'id' => 'numeric',
1121    ],
1122    'argument' => [
1123      'id' => 'numeric',
1124    ],
1125  ];
1126  $data['visitors_event']['pf_transfer'] = [
1127    'title' => t('Transfer'),
1128    'help' => t('Transfer performance.'),
1129    'field' => [
1130      'id' => 'numeric',
1131    ],
1132    'sort' => [
1133      'id' => 'standard',
1134    ],
1135    'filter' => [
1136      'id' => 'numeric',
1137    ],
1138    'argument' => [
1139      'id' => 'numeric',
1140    ],
1141  ];
1142  $data['visitors_event']['pf_dom_processing'] = [
1143    'title' => t('DOM Processing'),
1144    'help' => t('DOM Processing performance.'),
1145    'field' => [
1146      'id' => 'numeric',
1147    ],
1148    'sort' => [
1149      'id' => 'standard',
1150    ],
1151    'filter' => [
1152      'id' => 'numeric',
1153    ],
1154    'argument' => [
1155      'id' => 'numeric',
1156    ],
1157  ];
1158  $data['visitors_event']['pf_dom_complete'] = [
1159    'title' => t('DOM Complete'),
1160    'help' => t('DOM Complete performance.'),
1161    'field' => [
1162      'id' => 'numeric',
1163    ],
1164    'sort' => [
1165      'id' => 'standard',
1166    ],
1167    'filter' => [
1168      'id' => 'numeric',
1169    ],
1170    'argument' => [
1171      'id' => 'numeric',
1172    ],
1173  ];
1174  $data['visitors_event']['pf_on_load'] = [
1175    'title' => t('On Load'),
1176    'help' => t('On Load performance.'),
1177    'field' => [
1178      'id' => 'numeric',
1179    ],
1180    'sort' => [
1181      'id' => 'standard',
1182    ],
1183    'filter' => [
1184      'id' => 'numeric',
1185    ],
1186    'argument' => [
1187      'id' => 'numeric',
1188    ],
1189  ];
1190  $data['visitors_event']['pf_total'] = [
1191    'title' => t('Total'),
1192    'help' => t('Total performance.'),
1193    'field' => [
1194      'id' => 'numeric',
1195    ],
1196    'sort' => [
1197      'id' => 'standard',
1198    ],
1199    'filter' => [
1200      'id' => 'numeric',
1201    ],
1202    'argument' => [
1203      'id' => 'numeric',
1204    ],
1205  ];
1206  $data['visitors_event']['created'] = [
1207    'title' => t('Created'),
1208    'help' => t('The timestamp of the page view.'),
1209    'field' => [
1210      'id' => 'date',
1211      'click sortable' => TRUE,
1212    ],
1213    'filter' => [
1214      'id' => 'visitors_date',
1215    ],
1216  ];
1217  $data['visitors_event']['visitors_hour'] = [
1218    'title' => t('Hour'),
1219    'help' => t('The hour (server) of the visit.'),
1220    'field' => [
1221      'id' => 'visitors_hour',
1222      'field' => 'created',
1223    ],
1224    'sort' => [
1225      'id' => 'visitors_timestamp',
1226      'field' => 'created',
1227    ],
1228  ];
1229  $data['visitors_event']['visitors_month'] = [
1230    'title' => t('Month'),
1231    'help' => t('The month of the visit.'),
1232    'field' => [
1233      'id' => 'visitors_month',
1234      'field' => 'created',
1235    ],
1236    'sort' => [
1237      'id' => 'visitors_timestamp',
1238      'field' => 'created',
1239    ],
1240  ];
1241  $data['visitors_event']['visitors_local_hour'] = [
1242    'title' => t('Local hour'),
1243    'help' => t('Visitors local time.'),
1244    'field' => [
1245      'id' => 'visitors_local_hour',
1246      'field' => 'created',
1247    ],
1248    'sort' => [
1249      'id' => 'visitors_timestamp',
1250      'field' => 'created',
1251    ],
1252  ];
1253  $data['visitors_event']['visitors_day_of_week'] = [
1254    'title' => t('Day of Week'),
1255    'help' => t('The day of week of the visit.'),
1256    'field' => [
1257      'id' => 'visitors_day_of_week',
1258      'field' => 'created',
1259    ],
1260    'sort' => [
1261      'id' => 'visitors_timestamp',
1262      'field' => 'created',
1263    ],
1264  ];
1265  $data['visitors_event']['visitors_day_of_month'] = [
1266    'title' => t('Day of Month'),
1267    'help' => t('The day of month of the visit.'),
1268    'field' => [
1269      'id' => 'visitors_day_of_month',
1270      'field' => 'created',
1271    ],
1272    'sort' => [
1273      'id' => 'visitors_timestamp',
1274      'field' => 'created',
1275    ],
1276  ];
1277  $data['visitors_event']['visitors_day'] = [
1278    'title' => t('Day'),
1279    'help' => t('The day of the visit.'),
1280    'field' => [
1281      'id' => 'visitors_day',
1282      'field' => 'created',
1283    ],
1284    'sort' => [
1285      'id' => 'visitors_timestamp',
1286      'field' => 'created',
1287    ],
1288  ];
1289  $data['visitors_event']['visitors_week'] = [
1290    'title' => t('Week'),
1291    'help' => t('The week of the visit.'),
1292    'field' => [
1293      'id' => 'visitors_week',
1294      'field' => 'created',
1295    ],
1296    'sort' => [
1297      'id' => 'visitors_timestamp',
1298      'field' => 'created',
1299    ],
1300  ];
1301
1302  $data['visitors_visit']['visitors_display_link'] = [
1303    'title' => t('Link to Visitors display'),
1304    '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.'),
1305    'area' => [
1306      'id' => 'visitors_display_link',
1307    ],
1308  ];
1309
1310  return $data;
1311}
1312
1313/**
1314 * Implements hook_token_info().
1315 */
1316function visitors_token_info() {
1317  $entity['total-count'] = [
1318    'name' => t("Number of views"),
1319    'description' => t("The number of visitors who have read the node."),
1320  ];
1321  $entity['day-count'] = [
1322    'name' => t("Views today"),
1323    'description' => t("The number of visitors who have read the node today."),
1324  ];
1325  $entity['last-view'] = [
1326    'name' => t("Last view"),
1327    'description' => t("The date on which a visitor last read the node."),
1328    'type' => 'date',
1329  ];
1330
1331  $token = [
1332    'tokens' => [],
1333  ];
1334  $entity_types = \Drupal::config('visitors.settings')
1335    ->get('counter.entity_types') ?? [];
1336  foreach ($entity_types as $entity_type) {
1337    $token['tokens'][$entity_type] = $entity;
1338  }
1339
1340  return $token;
1341}
1342
1343/**
1344 * Implements hook_tokens().
1345 */
1346function visitors_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
1347  $token_service = \Drupal::token();
1348  $entity_types = \Drupal::config('visitors.settings')
1349    ->get('counter.entity_types') ?? [];
1350  $replacements = [];
1351
1352  if (!in_array($type, $entity_types) || empty($data[$type])) {
1353    return $replacements;
1354  }
1355  $entity = $data[$type];
1356
1357  /** @var \Drupal\visitors\VisitorsCounterInterface $counter_storage */
1358  $counter_storage = \Drupal::service('visitors.counter');
1359
1360  $entity_id = $entity->id() ?? 0;
1361  $entity_view = $counter_storage->fetchView($type, $entity_id);
1362  foreach ($tokens as $name => $original) {
1363    if ($name == 'total-count') {
1364      $replacements[$original] = $entity_view ? $entity_view->getTotalCount() : 0;
1365    }
1366    elseif ($name == 'day-count') {
1367      $replacements[$original] = $entity_view ? $entity_view->getDayCount() : 0;
1368    }
1369    elseif ($name == 'last-view') {
1370      $replacements[$original] = $entity_view ? \Drupal::service('date.formatter')->format($entity_view->getTimestamp()) : t('never');
1371    }
1372  }
1373
1374  if ($created_tokens = $token_service->findWithPrefix($tokens, 'last-view')) {
1375    $replacements += $token_service->generate('date', $created_tokens, ['date' => $entity_view ? $entity_view->getTimestamp() : 0], $options, $bubbleable_metadata);
1376  }
1377
1378  return $replacements;
1379}
1380
1381/**
1382 * Implements hook_form_alter().
1383 */
1384function visitors_form_alter(&$form, FormStateInterface $form_state, $form_id) {
1385  $static = &drupal_static(__FUNCTION__);
1386
1387  $form_object = $form_state->getFormObject();
1388  if (method_exists($form_object, 'getBaseFormId')) {
1389    $base_form_id = $form_object->getBaseFormId();
1390  }
1391  else {
1392    $base_form_id = $form_id;
1393  }
1394
1395  $static[$form_id] = $base_form_id;
1396
1397  $form['#submit'][] = [Form::class, 'submit'];
1398  $form['#validate'][] = [Form::class, 'validate'];
1399}
1400
1401/**
1402 * Implements hook_preprocess_html().
1403 */
1404function visitors_preprocess_html(array &$variables) {
1405  $title = '';
1406  if (isset($variables['head_title']['title'])) {
1407    $title = strip_tags((string) $variables['head_title']['title']);
1408  }
1409  elseif (isset($variables['head_title']) && is_string($variables['head_title'])) {
1410    $title = strip_tags($variables['head_title']);
1411  }
1412
1413  if ($title) {
1414    $variables['#attached']['drupalSettings']['visitors']['title'] = $title;
1415  }
1416}
1417
1418/**
1419 * Implements hook_preprocess_views_view_table().
1420 */
1421function visitors_preprocess_views_view_table(array &$variables) {
1422  // Check if this is a Visitors view that needs sort link cleaning.
1423  $view = $variables['view'];
1424  if ($view->storage->id() !== 'visitors') {
1425    return;
1426  }
1427
1428  // Get excluded parameters from static variable set in ReportController.
1429  $excluded_params = &drupal_static('visitors_excluded_sort_params', []);
1430  $referrer_path = &drupal_static('visitors_referrer_path', '');
1431
1432  if (empty($excluded_params)) {
1433    return;
1434  }
1435
1436  // Clean sort links by removing AJAX parameters.
1437  if (!empty($variables['header'])) {
1438    $current_query = \Drupal::request()->query->all();
1439
1440    // Remove excluded parameters.
1441    $clean_query = [];
1442    foreach ($current_query as $key => $value) {
1443      if (!in_array($key, $excluded_params)) {
1444        $clean_query[$key] = $value;
1445      }
1446    }
1447
1448    // Rebuild sort links for each header column.
1449    foreach ($variables['header'] as $field => &$header) {
1450      if (!empty($header['url'])) {
1451        // Parse the existing URL to get the sort parameters.
1452        $url_parts = parse_url($header['url']);
1453        $url_query = [];
1454        if (!empty($url_parts['query'])) {
1455          parse_str($url_parts['query'], $url_query);
1456        }
1457
1458        // Keep only the sort-related parameters and clean query.
1459        $sort_params = [];
1460        if (isset($url_query['order'])) {
1461          $sort_params['order'] = $url_query['order'];
1462        }
1463        if (isset($url_query['sort'])) {
1464          $sort_params['sort'] = $url_query['sort'];
1465        }
1466
1467        // Merge clean query with sort parameters.
1468        $final_query = array_merge($clean_query, $sort_params);
1469
1470        // Rebuild the URL.
1471        $route_name = $view->live_preview ? '<current>' : '<none>';
1472        if (!empty($referrer_path)) {
1473          // Use the referrer path for the URL.
1474          $header['url'] = $referrer_path;
1475          if (!empty($final_query)) {
1476            $header['url'] .= '?' . http_build_query($final_query);
1477          }
1478        }
1479        else {
1480          // Fallback to Drupal URL generation.
1481          $url = new Url($route_name, [], ['query' => $final_query]);
1482          $header['url'] = $url->toString();
1483        }
1484      }
1485    }
1486  }
1487}