From ecaed82a41d47c3a6f75edd3dfeb9a8e859381e8 Mon Sep 17 00:00:00 2001
From: Steve Wirt <Steve Wirt>
Date: Mon, 27 Jan 2025 21:58:40 -0500
Subject: [PATCH] Issue #3498465: Add cron option to rebuild the alt text audit

---
 README.md                                  | 23 +++++--
 alt_text_validation.module                 |  9 +++
 alt_text_validation.services.yml           |  1 +
 src/Form/AltTextValidationSettingsForm.php | 54 ++++++++++++----
 src/Service/Auditor.php                    | 71 ++++++++++++++++++++++
 src/Service/AuditorInterface.php           |  5 ++
 6 files changed, 146 insertions(+), 17 deletions(-)

diff --git a/README.md b/README.md
index 17d0807..2961b99 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-This module is not functional yet. It is only here for contributors to work on it.
+**This module is not functional yet.** It is only here for contributors to work on it.
 
 ## Introduction
 
@@ -32,18 +32,29 @@ See: https://www.drupal.org/node/895232 for further information.
 
 ## Configuration
 
-Go to /admin/config/content/alt-text-validation and configure.
+Configuration steps are minimal:
+
+  1. Go to /admin/config/content/alt-text-validation and configure.
+  2. Assign permissions for administering the module and viewing the report to Roles.
+
 
 ## Report
 
-The Alt Text Audit report is present at Reports >> Alt Text Report (/admin/reports/alt-text-report) for properly permed users.  This report is a View that can be modified if needed.  It requires  the Auditor job be run to populate the report. **The Auditor is not fully complete.  It currently only shows image data from fields of type 'image'.**
-To populate the report you will need to run
+The Alt Text Audit report is present at Reports >> Alt Text Report (/admin/reports/alt-text-report) for properly permed users.  This report is a View that can be modified if needed.  It requires the Auditor job be run to populate the report. The audit report will
+show any occurrences of image fields on any entity.  It also includes any html image tags found within text fields on any entity.
+
+There are currently two ways to initialize the report generation.  Depending on the
+size of your site, it may be an intensive process.
+To populate the report you will need to either:
+
+1. Enable report generation in cron (/admin/reports/alt-text-report) - It will begin the generation with the next cron run.
+2. Via Drush command.
 ```
 drush alt-text-validation:queue-audit
 drush cron
 ```
-It may take several cron runs to complete the audit.  You can assess the progress by
-`drush queue:list` then look for `atv_entity_instances`
+It uses the Queue API and may take several cron runs to fully populate the report. You can assess the progress by `drush queue:list` then look for `atv_entity_instances`
+The top of the report contains information about when the report was started and finished.
 
 ## Drush Commands
 
diff --git a/alt_text_validation.module b/alt_text_validation.module
index e626077..33b5db8 100644
--- a/alt_text_validation.module
+++ b/alt_text_validation.module
@@ -27,3 +27,12 @@ function alt_text_validation_help($route_name, RouteMatchInterface $route_match)
   }
   return NULL;
 }
+
+/**
+ * Implements hook_cron().
+ */
+function alt_text_validation_cron() {
+  /** @var \Drupal\alt_text_validation\Service\AuditorInterface $auditor */
+  $auditor = \Drupal::service('alt_text_validation.auditor');
+  $auditor->tryCron();
+}
diff --git a/alt_text_validation.services.yml b/alt_text_validation.services.yml
index ce927be..f8f1884 100644
--- a/alt_text_validation.services.yml
+++ b/alt_text_validation.services.yml
@@ -11,6 +11,7 @@ services:
     class: Drupal\alt_text_validation\Service\Auditor
     arguments:
       - '@alt_text_validation.audit_service'
+      - '@config.factory'
       - '@database'
       - '@entity_field.manager'
       - '@entity_type.manager'
diff --git a/src/Form/AltTextValidationSettingsForm.php b/src/Form/AltTextValidationSettingsForm.php
index 05d578e..1c170c8 100644
--- a/src/Form/AltTextValidationSettingsForm.php
+++ b/src/Form/AltTextValidationSettingsForm.php
@@ -14,7 +14,7 @@ class AltTextValidationSettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function getFormId() {
-    return 'alt_text_validation_settings_form';
+    return 'alt_text_validation_enabled_form';
   }
 
   /**
@@ -30,30 +30,60 @@ class AltTextValidationSettingsForm extends ConfigFormBase {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildForm($form, $form_state);
     $form['#title'] = $this->t('Alt Text Validation Settings');
-    $form['alt_text_validation_setting_group'] = [
+    $form['alt_text_validation_enabled_group'] = [
       '#type' => 'fieldset',
-      '#title' => $this->t('Validation status'),
     ];
-    $form['alt_text_validation_setting_group']['alt_text_validation_setting'] = [
+    $form['alt_text_validation_enabled_group']['alt_text_validation_enabled'] = [
       '#prefix' => '<div class="container-inline">',
+      '#title' => $this->t('Validate alt text on save'),
       '#type' => 'radios',
       '#options' => [
-        'validation_on' => $this->t('On (all active rules applied)'),
-        'validation_off' => $this->t('Off (no rules applied)'),
+        '1' => $this->t('On (all active rules applied)'),
+        '0' => $this->t('Off (no rules applied)'),
       ],
-      '#default_value' => $this->config('alt_text_validation.settings')->get('alt_text_validation_setting'),
+      '#default_value' => $this->config('alt_text_validation.settings')->get('alt_text_validation_enabled') ?? 0,
       '#suffix' => '</div>',
     ];
-    $form['alt_text_validation_setting_group_alert headings'] = [
+    $form['alt_text_validation_enabled_group']['cron_enabled'] = [
+      '#prefix' => '<div class="container-inline">',
+      '#title' => $this->t('Rebuild alt text report on cron'),
+      '#type' => 'radios',
+      '#options' => [
+        '1' => $this->t('On'),
+        '0' => $this->t('Off'),
+      ],
+      '#default_value' => $this->config('alt_text_validation.settings')->get('cron_enabled') ?? 0,
+      '#suffix' => '</div>',
+    ];
+    $form['alt_text_validation_enabled_group']['cron_delay'] = [
+      '#prefix' => '<div class="container-inline">',
+      '#title' => $this->t('Days between rebuild'),
+      '#type' => 'number',
+      '#attributes' => [
+        'data-type' => 'number',
+      ],
+      '#size' => 3,
+      '#max' => 365,
+      '#min' => 1,
+      '#default_value' => $this->config('alt_text_validation.settings')->get('cron_delay') ?? 7,
+      '#suffix' => '</div>',
+      '#states' => [
+        // Show this textfield only if the radio '1' is selected above.
+        'visible' => [
+          ':input[name="cron_enabled"]' => ['value' => '1'],
+        ],
+      ],
+    ];
+    $form['alt_text_validation_enabled_group_alert headings'] = [
       '#type' => 'fieldset',
       '#title' => $this->t('Customize alert headings'),
     ];
-    $form['alt_text_validation_setting_group_alert headings']['heading_warn'] = [
+    $form['alt_text_validation_enabled_group_alert headings']['heading_warn'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Warn heading text'),
       '#default_value' => $this->config('alt_text_validation.settings')->get('heading_warn') ? $this->config('alt_text_validation.settings')->get('heading_warn') : $this->t('Alt text warnings, please consider addressing them.'),
     ];
-    $form['alt_text_validation_setting_group_alert headings']['heading_prevent'] = [
+    $form['alt_text_validation_enabled_group_alert headings']['heading_prevent'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Prevent heading text'),
       '#default_value' => $this->config('alt_text_validation.settings')->get('heading_prevent') ? $this->config('alt_text_validation.settings')->get('heading_prevent') : $this->t('Alt text errors found. Page can not be saved.'),
@@ -66,7 +96,9 @@ class AltTextValidationSettingsForm extends ConfigFormBase {
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $config = $this->configFactory()->getEditable('alt_text_validation.settings');
-    $config->set('alt_text_validation_setting', $form_state->getValue('alt_text_validation_setting'))
+    $config->set('alt_text_validation_enabled', $form_state->getValue('alt_text_validation_enabled'))
+      ->set('cron_enabled', $form_state->getValue('cron_enabled'))
+      ->set('cron_delay', $form_state->getValue('cron_delay'))
       ->set('heading_warn', $form_state->getValue('heading_warn'))
       ->set('heading_prevent', $form_state->getValue('heading_prevent'))
       ->save();
diff --git a/src/Service/Auditor.php b/src/Service/Auditor.php
index 81aae94..9a6c476 100644
--- a/src/Service/Auditor.php
+++ b/src/Service/Auditor.php
@@ -3,6 +3,7 @@
 namespace Drupal\alt_text_validation\Service;
 
 use Drupal\alt_text_validation\AtvCommonTrait;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
@@ -17,9 +18,17 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * Class for Auditor to process and build the audit.
  */
 class Auditor implements AuditorInterface, ContainerInjectionInterface {
+
   use AtvCommonTrait;
   use StringTranslationTrait;
 
+  /**
+   * The configuration for alt_text_validation.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $atvConfig;
+
   /**
    * The Alt Text Validation audit storage service.
    *
@@ -74,6 +83,8 @@ class Auditor implements AuditorInterface, ContainerInjectionInterface {
    *
    * @param Drupal\alt_text_validation\Service\AuditStorageInterface $audit_storage
    *   The audit storage service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
    * @param Drupal\Core\Database\Connection $database_connection
    *   The database connection service.
    * @param Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
@@ -89,6 +100,7 @@ class Auditor implements AuditorInterface, ContainerInjectionInterface {
    */
   final public function __construct(
     AuditStorageInterface $audit_storage,
+    ConfigFactoryInterface $config_factory,
     Connection $database_connection,
     EntityFieldManagerInterface $entity_field_manager,
     EntityTypeManagerInterface $entity_type_manager,
@@ -97,6 +109,7 @@ class Auditor implements AuditorInterface, ContainerInjectionInterface {
     State $state,
   ) {
     $this->auditStorage = $audit_storage;
+    $this->atvConfig = $config_factory->getEditable('alt_text_validation.settings');
     $this->databaseConnection = $database_connection;
     $this->entityFieldManager = $entity_field_manager;
     $this->entityTypeManager = $entity_type_manager;
@@ -111,6 +124,7 @@ class Auditor implements AuditorInterface, ContainerInjectionInterface {
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('alt_text_validation.audit_storage'),
+      $container->get('config.factory'),
       $container->get('database'),
       $container->get('entity_field.manager'),
       $container->get('entity_type.manager'),
@@ -186,4 +200,61 @@ class Auditor implements AuditorInterface, ContainerInjectionInterface {
     $this->queue->get(self::getEntityInstanceQueueName())->deleteQueue();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function tryCron(): void {
+    // Check settings to see if cron processing is enabled.
+    if (!empty($this->atvConfig->get('cron_enabled'))) {
+      // Check the last completed time.
+      $times = $this->state->getMultiple([
+        self::getAuditStartTimeKey(),
+        self::getAuditEndTimeKey(),
+      ]);
+      $running = !empty($times[self::getAuditStartTimeKey()]) && empty($times[self::getAuditEndTimeKey()]);
+      $delay_is_right = $this->checkDelay($times[self::getAuditEndTimeKey()]);
+      if ((!$running && $delay_is_right) || $this->checkTooLong($times[self::getAuditStartTimeKey()])) {
+        $this->queueAllImages();
+      }
+    }
+  }
+
+  /**
+   * Checks if the delay between audits is sufficient.
+   *
+   * @param int|null $last_end_time
+   *   The timestamp of the last completed audit.
+   *
+   * @return bool
+   *   TRUE if the delay was sufficient, FALSE otherwise.
+   */
+  protected function checkDelay(int|null $last_end_time): bool {
+    if (empty($last_end_time)) {
+      // It was not last run, so it is ready to run.
+      return TRUE;
+    }
+    $delay = $this->atvConfig->get('audit_delay') * 24 * 60 * 60;
+    return (time() - $last_end_time) >= $delay;
+  }
+
+  /**
+   * Checks if it has been too long since run started, or never run.
+   *
+   * @param int|null $last_start_time
+   *   The timestamp of the last completed audit.
+   *
+   * @return bool
+   *   TRUE if the delay was sufficient, FALSE otherwise.
+   */
+  protected function checkTooLong(int|null $last_start_time): bool {
+    if (empty($last_start_time)) {
+      // It has never run, so it is ready to run.
+      return TRUE;
+    }
+    // We will consider an additional 2 days beyond should mean something is
+    // stuck so run this again.
+    $delay = ($this->atvConfig->get('audit_delay') + 2) * 24 * 60 * 60;
+    return (time() - ($last_start_time)) >= $delay;
+  }
+
 }
diff --git a/src/Service/AuditorInterface.php b/src/Service/AuditorInterface.php
index c97ac8b..38f44bf 100644
--- a/src/Service/AuditorInterface.php
+++ b/src/Service/AuditorInterface.php
@@ -12,4 +12,9 @@ interface AuditorInterface {
    */
   public function queueAllImages(): void;
 
+  /**
+   * Checks to see if it is appropriate to run cron.
+   */
+  public function tryCron(): void;
+
 }
-- 
GitLab