Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
204 / 204
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
SettingsForm
100.00% covered (success)
100.00%
204 / 204
100.00% covered (success)
100.00%
9 / 9
18
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getFormId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditableConfigNames
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 buildForm
100.00% covered (success)
100.00%
126 / 126
100.00% covered (success)
100.00%
1 / 1
3
 validateForm
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 submitForm
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 buildVisibilityConditions
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
4
 entityTypes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\visitors\Form;
6
7use Drupal\Core\Condition\ConditionManager;
8use Drupal\Core\Config\Entity\ConfigEntityType;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Form\ConfigFormBase;
11use Drupal\Core\Form\FormStateInterface;
12use Drupal\Core\Form\SubformState;
13use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
14use Drupal\Core\Url;
15use Drupal\visitors\VisitorsVisibilityInterface;
16use Symfony\Component\DependencyInjection\ContainerInterface;
17
18/**
19 * Visitors Settings Form.
20 */
21final class SettingsForm extends ConfigFormBase {
22
23  /**
24   * Visitors settings.
25   *
26   * @var string
27   */
28  const SETTINGS = 'visitors.settings';
29
30  /**
31   * An extension discovery instance.
32   *
33   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
34   */
35  protected EntityTypeManagerInterface $entityTypeManager;
36
37  /**
38   * The condition plugin manager.
39   *
40   * @var \Drupal\Core\Condition\ConditionManager
41   */
42  protected ConditionManager $conditionManager;
43
44  /**
45   * The context repository service.
46   *
47   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
48   */
49  protected ContextRepositoryInterface $contextRepository;
50
51  /**
52   * Constructs a Settings form.
53   *
54   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
55   *   The entity type manager.
56   * @param \Drupal\Core\Condition\ConditionManager $condition_manager
57   *   The condition plugin manager.
58   * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
59   *   The context repository.
60   */
61  public function __construct(
62    EntityTypeManagerInterface $entity_type_manager,
63    ConditionManager $condition_manager,
64    ContextRepositoryInterface $context_repository,
65  ) {
66    $this->entityTypeManager = $entity_type_manager;
67    $this->conditionManager  = $condition_manager;
68    $this->contextRepository = $context_repository;
69  }
70
71  /**
72   * {@inheritdoc}
73   */
74  public static function create(ContainerInterface $container) {
75    return new self(
76      $container->get('entity_type.manager'),
77      $container->get('plugin.manager.condition'),
78      $container->get('context.repository')
79    );
80  }
81
82  /**
83   * {@inheritdoc}
84   */
85  public function getFormId() {
86    return 'visitors_admin_settings';
87  }
88
89  /**
90   * {@inheritdoc}
91   */
92  protected function getEditableConfigNames() {
93    return [
94      static::SETTINGS,
95    ];
96  }
97
98  /**
99   * {@inheritdoc}
100   */
101  public function buildForm(array $form, FormStateInterface $form_state) {
102    $settings = $this->config(self::SETTINGS);
103    $form = parent::buildForm($form, $form_state);
104
105    $form['#attached']['library'][] = 'visitors/visitors.admin';
106
107    $form['visitors_disable_tracking'] = [
108      '#type' => 'radios',
109      '#title' => $this->t('Track visitors'),
110      '#options' => [
111        $this->t('Enabled'),
112        $this->t('Disabled'),
113      ],
114      '#description' => $this->t('Enable or disable tracking of visitors.'),
115      '#default_value' => (int) $settings->get('disable_tracking'),
116    ];
117
118    $script_type = $settings->get('script_type');
119    $form['script_type'] = [
120      '#type' => 'radios',
121      '#title' => $this->t('Script type'),
122      '#options' => [
123        'minified' => $this->t('Minified'),
124        'full' => $this->t('Full'),
125      ],
126      '#default_value' => $script_type == 'full' ? 'full' : 'minified',
127      '#description' => $this->t('Full script is for debugging purposes. Minified script is for production.'),
128    ];
129
130    $form['user_visibility_settings'] = [
131      '#type' => 'markup  ',
132      '#markup' => '<h2>' . $this->t('User Settings') . '</h2>',
133    ];
134
135    $visibility_user_account_mode = $settings->get('visibility.user_account_mode');
136
137    $t_permission = ['%permission' => $this->t('opt-out of visitors tracking')];
138    $form['user_visibility_settings']['visitors_visibility_user_account_mode'] = [
139      '#type' => 'radios',
140      '#title' => $this->t('Allow users to customize tracking on their account page'),
141      '#options' => [
142        VisitorsVisibilityInterface::USER_NO_PERSONALIZATION => $this->t('No customization allowed'),
143        VisitorsVisibilityInterface::USER_OPT_OUT => $this->t('Tracking on by default, users with %permission permission can opt out', $t_permission),
144        VisitorsVisibilityInterface::USER_OPT_IN => $this->t('Tracking off by default, users with %permission permission can opt in', $t_permission),
145      ],
146      '#default_value' => !empty($visibility_user_account_mode) ? $visibility_user_account_mode : 0,
147    ];
148    $form['user_visibility_settings']['visitors_trackuserid'] = [
149      '#type' => 'checkbox',
150      '#title' => $this->t('Track User ID'),
151      '#default_value' => $settings->get('track.userid'),
152      '#description' => $this->t('User ID enables the analysis of groups of sessions, across devices, using a unique, persistent, and representing a user. <a href=":url">Learn more about the benefits of using User ID</a>.', [':url' => 'https://matomo.org/docs/user-id/']),
153    ];
154
155    $form['user_visibility_settings']['visibility_exclude_user1'] = [
156      '#type' => 'checkbox',
157      '#title' => $this->t('Exclude user1 from statistics'),
158      '#default_value' => $settings->get('visibility.exclude_user1'),
159      '#description' => $this->t('Exclude hits of user1 from statistics.'),
160    ];
161
162    $form['retention'] = [
163      '#type' => 'markup',
164      '#markup' => '<h2>' . $this->t('Log Retention') . '</h2>',
165    ];
166
167    $form['retention']['flush_log_timer'] = [
168      '#type' => 'select',
169      '#title' => $this->t('Discard visitors logs older than'),
170      '#default_value'   => $settings->get('flush_log_timer'),
171      '#options' => [
172        0 => $this->t('Never'),
173        3600 => $this->t('1 hour'),
174        10800 => $this->t('3 hours'),
175        21600 => $this->t('6 hours'),
176        32400 => $this->t('9 hours'),
177        43200 => $this->t('12 hours'),
178        86400 => $this->t('1 day'),
179        172800 => $this->t('2 days'),
180        259200 => $this->t('3 days'),
181        604800 => $this->t('1 week'),
182        1209600 => $this->t('2 weeks'),
183        4838400 => $this->t('1 month 3 weeks'),
184        9676800 => $this->t('3 months 3 weeks'),
185        31536000 => $this->t('1 year'),
186        34214400 => $this->t('13 months'),
187      ],
188      '#description' =>
189      $this->t('Older visitors log entries (including referrer statistics) will be automatically discarded. (Requires a correctly configured <a href="@cron">cron maintenance task</a>.)',
190          ['@cron' => Url::fromRoute('system.status')->toString()]
191      ),
192    ];
193
194    $form['retention']['bot_retention_log'] = [
195      '#type' => 'select',
196      '#title' => $this->t('Discard bot logs older than'),
197      '#default_value'   => $settings->get('bot_retention_log'),
198      '#options' => [
199        -1 => $this->t('Do not log'),
200        0 => $this->t('Never'),
201        3600 => $this->t('1 hour'),
202        10800 => $this->t('3 hours'),
203        21600 => $this->t('6 hours'),
204        32400 => $this->t('9 hours'),
205        43200 => $this->t('12 hours'),
206        86400 => $this->t('1 day'),
207        172800 => $this->t('2 days'),
208        259200 => $this->t('3 days'),
209        604800 => $this->t('1 week'),
210        1209600 => $this->t('2 weeks'),
211        4838400 => $this->t('1 month 3 weeks'),
212        9676800 => $this->t('3 months 3 weeks'),
213        31536000 => $this->t('1 year'),
214        34214400 => $this->t('13 months'),
215      ],
216      '#description' =>
217      $this->t('Control how long or if visits by bots are logged.'),
218    ];
219
220    $form['entity'] = [
221      '#type' => 'markup',
222      '#markup' => '<h2>' . $this->t('Entity Views Counter') . '</h2>',
223    ];
224
225    $form['entity']['counter_enabled'] = [
226      '#type' => 'checkbox',
227      '#title' => $this->t('Count entity views'),
228      '#default_value' => $settings->get('counter.enabled'),
229      '#description' => $this->t('Count the number of times entities are viewed.'),
230    ];
231    $form['entity']['entity_types'] = [
232      '#type' => 'checkboxes',
233      '#title' => $this->t('Entity Types'),
234      '#options' => $this->entityTypes(),
235      '#default_value' => $settings->get('counter.entity_types') ?? [],
236      '#description' => $this->t('Which entity types should be tracked.'),
237    ];
238
239    // Build condition-based visibility UI.
240    $this->buildVisibilityConditions($form, $form_state, $settings);
241
242    return $form;
243  }
244
245  /**
246   * {@inheritdoc}
247   */
248  public function validateForm(array &$form, FormStateInterface $form_state): void {
249    parent::validateForm($form, $form_state);
250
251    // Validate visibility condition settings.
252    foreach ($form_state->getValue('visibility') as $condition_id => $condition_values) {
253      // Get the condition plugin instance.
254      $condition = $form_state->get(['conditions', $condition_id]);
255      // Validate the condition configuration.
256      $condition->validateConfigurationForm(
257        $form['visibility'][$condition_id],
258        SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state)
259      );
260    }
261  }
262
263  /**
264   * {@inheritdoc}
265   */
266  public function submitForm(array &$form, FormStateInterface $form_state): void {
267    $settings = $this->config(self::SETTINGS);
268    $values = $form_state->getValues();
269
270    // Process tracking conditions.
271    $tracking_conditions = [];
272    foreach ($form_state->getValue('visibility') as $condition_id => $condition_values) {
273      // Get the condition plugin instance.
274      $condition = $form_state->get(['conditions', $condition_id]);
275      // Submit the condition configuration.
276      $condition->submitConfigurationForm(
277        $form['visibility'][$condition_id],
278        SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state)
279      );
280      // Store the configuration.
281      $tracking_conditions[$condition_id] = $condition->getConfiguration();
282    }
283
284    $settings
285      ->set('flush_log_timer', $values['flush_log_timer'])
286      ->set('bot_retention_log', $values['bot_retention_log'])
287      ->set('track.userid', $values['visitors_trackuserid'])
288      ->set('counter.enabled', $values['counter_enabled'])
289      ->set('counter.entity_types', array_filter($values['entity_types'] ?? []))
290      ->set('disable_tracking', $values['visitors_disable_tracking'])
291      ->set('visibility.user_account_mode', $values['visitors_visibility_user_account_mode'])
292      ->set('script_type', $values['script_type'] ?? 'minified')
293      ->set('tracking_conditions', $tracking_conditions)
294      ->save();
295
296    parent::submitForm($form, $form_state);
297  }
298
299  /**
300   * Helper function for building the visibility UI form.
301   *
302   * @param array $form
303   *   An associative array containing the structure of the form.
304   * @param \Drupal\Core\Form\FormStateInterface $form_state
305   *   The current state of the form.
306   * @param \Drupal\Core\Config\ImmutableConfig $settings
307   *   The configuration object.
308   *
309   * @return array
310   *   The form array with the visibility UI added in.
311   */
312  protected function buildVisibilityConditions(array &$form, FormStateInterface $form_state, $settings): array {
313    $form['visibility_tabs'] = [
314      '#type' => 'vertical_tabs',
315      '#title' => $this->t('Visibility'),
316      '#title_display' => 'invisible',
317      '#parents' => ['visibility_tabs'],
318      '#group' => 'visibility_tabs',
319    ];
320
321    // Get current tracking conditions.
322    $tracking_conditions = $settings->get('tracking_conditions') ?: [];
323
324    // Get available contexts for condition plugins.
325    $contexts = $this->contextRepository->getAvailableContexts();
326    $form_state->setTemporaryValue('gathered_contexts', $contexts);
327
328    // Initialize visibility container.
329    $form['visibility'] = [
330      '#type' => 'container',
331      '#tree' => TRUE,
332    ];
333
334    $excluded_conditions = $settings->get('excluded_conditions') ?: [];
335    // Get all available condition plugin definitions.
336    $all_definitions = $this->conditionManager->getDefinitions();
337    $definitions = array_diff_key($all_definitions, array_flip($excluded_conditions));
338    foreach ($definitions as $condition_id => $definition) {
339      // Get existing condition configuration or create new instance.
340      $condition_config = $tracking_conditions[$condition_id] ?? ['id' => $condition_id];
341      $condition = $this->conditionManager->createInstance(
342        $condition_id,
343        $condition_config
344      );
345
346      // Store condition for validation and submission.
347      $form_state->set(['conditions', $condition_id], $condition);
348
349      // Build condition form.
350      $condition_form = $condition->buildConfigurationForm([], $form_state);
351      $condition_form['#type'] = 'details';
352      $condition_form['#title'] = $definition['label'];
353      $condition_form['#group'] = 'visibility_tabs';
354
355      $form['visibility'][$condition_id] = $condition_form;
356    }
357
358    return $form;
359  }
360
361  /**
362   * Returns a list of entity types.
363   */
364  protected function entityTypes() {
365    $entity_types_list = [];
366    $entity_definitions = $this->entityTypeManager->getDefinitions();
367    foreach ($entity_definitions as $entity_name => $entity_definition) {
368      if ($entity_definition instanceof ConfigEntityType) {
369        continue;
370      }
371      $entity_types_list[$entity_name] = (string) $entity_definition->getLabel();
372    }
373    asort($entity_types_list);
374
375    return $entity_types_list;
376  }
377
378}