From a5a75d821954e3d53a23f5be2e60b7c7437da731 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 13:18:00 +0200
Subject: [PATCH 01/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../src/OEmbed/ResourceFetcherDecorator.php   |  46 ++++++
 .../src/XrayAuditInsightReport.php            |  89 ++++++++++
 .../xray_audit_insight.install                |  58 +++++++
 .../xray_audit_insight.services.yml           |  11 ++
 .../XrayAuditExternalResourcesPlugin.php      | 154 ++++++++++++++++++
 5 files changed, 358 insertions(+)
 create mode 100644 modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
 create mode 100644 modules/xray_audit_insight/src/XrayAuditInsightReport.php
 create mode 100644 modules/xray_audit_insight/xray_audit_insight.install
 create mode 100644 src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php

diff --git a/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php b/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
new file mode 100644
index 0000000..9002117
--- /dev/null
+++ b/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\xray_audit_insight\OEmbed;
+
+use Drupal\media\OEmbed\ResourceFetcherInterface;
+use Drupal\media\OEmbed\ResourceException;
+use Drupal\xray_audit_insight\XrayAuditInsightReport;
+
+/**
+ * Decorates the original ResourceFetcher service to add logging capabilities.
+ */
+class ResourceFetcherDecorator implements ResourceFetcherInterface {
+
+  /**
+   * Constructs a new ResourceFetcherDecorator.
+   *
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
+   *   The original resource fetcher.
+   * @param \Drupal\xray_audit_insight\XrayAuditInsightReport $insight_report
+   *   The XrayAuditInsight report service.
+   */
+  public function __construct(
+    protected ResourceFetcherInterface $resourceFetcher,
+    protected XrayAuditInsightReport $insightReport,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchResource($url) {
+    try {
+      return $this->resourceFetcher->fetchResource($url);
+    }
+    catch (ResourceException $e) {
+      // Log the exception to the xray_audit_insight table.
+      $this->insightReport->addInsightData(
+        'external_resource',
+        $e->getMessage(),
+        $url
+      );
+
+      throw $e;
+    }
+  }
+
+}
diff --git a/modules/xray_audit_insight/src/XrayAuditInsightReport.php b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
new file mode 100644
index 0000000..d48462a
--- /dev/null
+++ b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\xray_audit_insight;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Service for managing the xray_audit_insight table.
+ */
+class XrayAuditInsightReport {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Constructs a new XrayAuditInsightReport object.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * Retrieves insights by type.
+   *
+   * @param string $type
+   *   The type of insights to retrieve.
+   *
+   * @return array
+   *   An array of insights.
+   */
+  public function getInsightsByType($type) {
+    $query = $this->database->select('xray_audit_insight', 'xai')
+      ->fields('xai')
+      ->condition('type', $type);
+
+    return $query->execute()->fetchAll();
+  }
+
+  /**
+   * Adds a new insight with text data to the xray_audit_insight table.
+   *
+   * Only adds the record if no record with the same type and data exists.
+   *
+   * @param string $type
+   *   The type of the insight.
+   * @param string $message
+   *   The message associated with the insight.
+   * @param string $url
+   *   The URL to be stored.
+   *
+   * @return bool
+   *   TRUE if a new record was added, FALSE if a duplicate was found.
+   */
+  public function addInsightData(string $type, string $message, string $url) {
+    $data = $url;
+
+    // Check if a record with the same type and data already exists.
+    $exists = $this->database->select('xray_audit_insight', 'xai')
+      ->fields('xai', ['id'])
+      ->condition('type', $type)
+      ->condition('data', $data)
+      ->condition('message', $message)
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+
+    // Only insert if no matching record exists.
+    if (!$exists) {
+      $this->database->insert('xray_audit_insight')
+        ->fields([
+          'type' => $type,
+          'data' => $data,
+          'message' => $message,
+        ])
+        ->execute();
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/modules/xray_audit_insight/xray_audit_insight.install b/modules/xray_audit_insight/xray_audit_insight.install
new file mode 100644
index 0000000..89204df
--- /dev/null
+++ b/modules/xray_audit_insight/xray_audit_insight.install
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the xray_audit_insight module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function xray_audit_insight_schema() {
+  $schema['xray_audit_insight'] = [
+    'description' => 'Stores xray audit insight data',
+    'fields' => [
+      'id' => [
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique insight ID.',
+      ],
+      'type' => [
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'description' => 'Insight type.',
+      ],
+      'message' => [
+        'type' => 'text',
+        'size' => 'big',
+        'not null' => FALSE,
+        'description' => 'Text data containing insight information.',
+      ],
+      'data' => [
+        'type' => 'text',
+        'size' => 'big',
+        'not null' => FALSE,
+        'description' => 'Text data containing insight information.',
+      ],
+    ],
+    'primary key' => ['id'],
+    'indexes' => [
+      'type' => ['type'],
+    ],
+  ];
+
+  return $schema;
+}
+
+/**
+ * Create xray_audit_insight table.
+ */
+function xray_audit_insight_update_9001() {
+  $schema = \Drupal::database()->schema();
+  $table_name = 'xray_audit_insight';
+
+  if (!$schema->tableExists($table_name)) {
+    $schema->createTable($table_name, xray_audit_insight_schema()[$table_name]);
+  }
+}
diff --git a/modules/xray_audit_insight/xray_audit_insight.services.yml b/modules/xray_audit_insight/xray_audit_insight.services.yml
index 0a6dcb5..fec69cc 100644
--- a/modules/xray_audit_insight/xray_audit_insight.services.yml
+++ b/modules/xray_audit_insight/xray_audit_insight.services.yml
@@ -2,3 +2,14 @@ services:
   plugin_manager.xray_audit_insight:
     class: Drupal\xray_audit_insight\Plugin\XrayAuditInsightPluginManager
     parent: default_plugin_manager
+
+  xray_audit_insight.report:
+    class: Drupal\xray_audit_insight\XrayAuditInsightReport
+    arguments: ['@database']
+
+  xray_audit_insight.resource_fetcher:
+    class: Drupal\xray_audit_insight\OEmbed\ResourceFetcherDecorator
+    arguments: [ '@xray_audit_insight.resource_fetcher.inner', '@xray_audit_insight.report']
+    decorates: media.oembed.resource_fetcher
+    decoration_priority: 10
+    public: false
diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
new file mode 100644
index 0000000..5ca2e31
--- /dev/null
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Drupal\xray_audit\Plugin\xray_audit\tasks\ContentDisplay;
+
+use Drupal\xray_audit\Plugin\XrayAuditTaskPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @XrayAuditTaskPlugin (
+ *   id = "external_resources",
+ *   label = @Translation("External Resources"),
+ *   description = @Translation("External resources referenced in content."),
+ *   group = "content_display",
+ *   sort = 2,
+ *   operations = {
+ *     "external_resources" = {
+ *       "label" = "External Resources",
+ *       "description" = "Lists external resources referenced in content"
+ *     }
+ *   }
+ * )
+ */
+class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+    $instance->entityTypeManager = $container->get('entity_type.manager');
+    $instance->entityFieldManager = $container->get('entity_field.manager');
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDataOperationResult(string $operation = '') {
+    $results = [];
+    $media_storage = $this->entityTypeManager->getStorage('media');
+    $display_storage = $this->entityTypeManager->getStorage('entity_view_display');
+
+    // Get all media bundles.
+    $bundles = $this->entityTypeManager->getStorage('media_type')->loadMultiple();
+
+    // Track fields using OEmbedFormatter.
+    $oembed_fields = [];
+
+    // Find all view displays using OEmbedFormatter.
+    foreach ($bundles as $bundle_id => $bundle) {
+      // Check all view modes for this bundle.
+      $displays = $display_storage->loadByProperties([
+        'targetEntityType' => 'media',
+        'bundle' => $bundle_id,
+      ]);
+
+      foreach ($displays as $display) {
+        $components = $display->getComponents();
+        foreach ($components as $field_name => $component) {
+          if (isset($component['type']) && str_contains($component['type'], 'oembed')) {
+            $oembed_fields[$bundle_id][$field_name] = $field_name;
+          }
+        }
+      }
+    }
+
+    // Process each bundle that has oembed fields.
+    foreach ($oembed_fields as $bundle_id => $field_names) {
+      // Get all media entities of this bundle.
+      $media_ids = $media_storage->getQuery()
+        ->condition('bundle', $bundle_id)
+        ->accessCheck(FALSE)
+        ->execute();
+
+      if (empty($media_ids)) {
+        continue;
+      }
+
+      $media_entities = $media_storage->loadMultiple($media_ids);
+
+      // Process each media entity.
+      foreach ($media_entities as $media_id => $media) {
+        foreach ($field_names as $field_name) {
+          if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
+            // For oembed fields, the URL is typically in the 'value' property.
+            $field_items = $media->get($field_name);
+
+            foreach ($field_items as $field_item) {
+              if ($field_item->getValue() && isset($field_item->getValue()['value'])) {
+                $results[] = (object) [
+                  'entity_id' => $media_id,
+                  'entity_type' => 'media',
+                  'bundle' => $bundle_id,
+                  'resource' => $field_item->getValue()['value'],
+                  'edit_url' => $media->toUrl('edit-form'),
+                ];
+              }
+            }
+          }
+        }
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildDataRenderArray(array $data, string $operation = '') {
+    $build = [];
+
+    $rows = [];
+    foreach ($data as $row) {
+      $rows[] = [
+        'entity_id' => $row->entity_id,
+        'entity_type' => $row->entity_type,
+        'bundle' => $row->bundle,
+        'resource' => $row->resource,
+        'edit_link' => \Drupal::linkGenerator()->generate($this->t('Edit'), $row->edit_url),
+      ];
+    }
+
+    $build['table'] = [
+      '#theme' => 'table',
+      '#header' => [
+        'Entity ID',
+        'Entity Type',
+        'Bundle',
+        'Resource URL',
+        'Edit',
+      ],
+      '#rows' => $rows,
+      '#empty' => $this->t('No external resources found'),
+    ];
+
+    return $build;
+  }
+
+}
-- 
GitLab


From 43419df86d4026b7188b53e031a32f107c25dbc5 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 13:40:05 +0200
Subject: [PATCH 02/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../XrayAuditExternalResourcesPlugin.php      | 101 ++++++++++++++++--
 1 file changed, 91 insertions(+), 10 deletions(-)

diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index 5ca2e31..314422e 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -6,6 +6,8 @@ use Drupal\xray_audit\Plugin\XrayAuditTaskPluginBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
+ * Plugin implementation of external resources.
+ *
  * @XrayAuditTaskPlugin (
  *   id = "external_resources",
  *   label = @Translation("External Resources"),
@@ -23,15 +25,40 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
 
   /**
+   * The entity type manager service.
+   *
    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
    */
   protected $entityTypeManager;
 
   /**
+   * The entity field manager service.
+   *
    * @var \Drupal\Core\Entity\EntityFieldManagerInterface
    */
   protected $entityFieldManager;
 
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The XrayAuditInsightReport service.
+   *
+   * @var \Drupal\xray_audit_insight\XrayAuditInsightReport|null
+   */
+  protected $insightReport;
+
+  /**
+   * The URL resolver service.
+   *
+   * @var \Drupal\media\OEmbed\UrlResolverInterface
+   */
+  protected $urlResolver;
+
   /**
    * {@inheritdoc}
    */
@@ -43,6 +70,17 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
     );
     $instance->entityTypeManager = $container->get('entity_type.manager');
     $instance->entityFieldManager = $container->get('entity_field.manager');
+    $instance->moduleHandler = $container->get('module_handler');
+
+    if ($instance->moduleHandler->moduleExists('media')) {
+      $instance->urlResolver = $container->get('media.oembed.url_resolver');
+    }
+    // Check if xray_audit_insight module is enabled.
+    if ($instance->moduleHandler->moduleExists('xray_audit_insight')) {
+      $instance->insightReport = $container->get('xray_audit_insight.report');
+
+    }
+
     return $instance;
   }
 
@@ -51,6 +89,9 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
    */
   public function getDataOperationResult(string $operation = '') {
     $results = [];
+    if (!$this->moduleHandler->moduleExists('media')) {
+      return $results;
+    }
     $media_storage = $this->entityTypeManager->getStorage('media');
     $display_storage = $this->entityTypeManager->getStorage('entity_view_display');
 
@@ -101,13 +142,37 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
 
             foreach ($field_items as $field_item) {
               if ($field_item->getValue() && isset($field_item->getValue()['value'])) {
-                $results[] = (object) [
+                $resource_url = $field_item->getValue()['value'];
+                $result = [
                   'entity_id' => $media_id,
                   'entity_type' => 'media',
                   'bundle' => $bundle_id,
-                  'resource' => $field_item->getValue()['value'],
+                  'resource' => $resource_url,
                   'edit_url' => $media->toUrl('edit-form'),
+                  'needs_review' => FALSE,
+                  'insight_message' => '',
                 ];
+
+                // Check if xray_audit_insight module is enabled
+                // and query for insights.
+                if ($this->insightReport) {
+                  // Get insights of type 'external_resource' where
+                  // data matches the normalized resource URL.
+                  $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
+                  $query = \Drupal::database()->select('xray_audit_insight', 'xai')
+                    ->fields('xai')
+                    ->condition('type', 'external_resource')
+                    ->condition('data', $normalized_url);
+                  $insights = $query->execute()->fetchAll();
+
+                  if (!empty($insights)) {
+                    $result['needs_review'] = TRUE;
+                    // Use the message from the most recent insight.
+                    $result['insight_message'] = end($insights)->message;
+                  }
+                }
+
+                $results[] = (object) $result;
               }
             }
           }
@@ -126,24 +191,40 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
 
     $rows = [];
     foreach ($data as $row) {
-      $rows[] = [
+      $row_data = [
         'entity_id' => $row->entity_id,
         'entity_type' => $row->entity_type,
         'bundle' => $row->bundle,
         'resource' => $row->resource,
         'edit_link' => \Drupal::linkGenerator()->generate($this->t('Edit'), $row->edit_url),
       ];
+
+      // Add insight columns if xray_audit_insight module is enabled.
+      if ($this->moduleHandler->moduleExists('xray_audit_insight')) {
+        $row_data['needs_review'] = isset($row->needs_review) && $row->needs_review ? $this->t('Yes') : $this->t('No');
+        $row_data['insight_message'] = $row->insight_message ?? '';
+      }
+
+      $rows[] = $row_data;
+    }
+
+    $header = [
+      'Entity ID',
+      'Entity Type',
+      'Bundle',
+      'Resource URL',
+      'Edit',
+    ];
+
+    // Add insight headers if xray_audit_insight module is enabled.
+    if ($this->moduleHandler->moduleExists('xray_audit_insight')) {
+      $header[] = $this->t('Needs Review');
+      $header[] = $this->t('Message');
     }
 
     $build['table'] = [
       '#theme' => 'table',
-      '#header' => [
-        'Entity ID',
-        'Entity Type',
-        'Bundle',
-        'Resource URL',
-        'Edit',
-      ],
+      '#header' => $header,
       '#rows' => $rows,
       '#empty' => $this->t('No external resources found'),
     ];
-- 
GitLab


From 95aba9cc29861f269ceffd0458b156117c80a9c3 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 14:23:29 +0200
Subject: [PATCH 03/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../XrayAuditExternalResourcesInsight.php     | 85 +++++++++++++++++++
 .../XrayAuditExternalResourcesPlugin.php      | 32 +++++++
 2 files changed, 117 insertions(+)
 create mode 100644 modules/xray_audit_insight/src/Plugin/insights/XrayAuditExternalResourcesInsight.php

diff --git a/modules/xray_audit_insight/src/Plugin/insights/XrayAuditExternalResourcesInsight.php b/modules/xray_audit_insight/src/Plugin/insights/XrayAuditExternalResourcesInsight.php
new file mode 100644
index 0000000..23ebd00
--- /dev/null
+++ b/modules/xray_audit_insight/src/Plugin/insights/XrayAuditExternalResourcesInsight.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\xray_audit_insight\Plugin\insights;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\xray_audit_insight\Plugin\XrayAuditInsightPluginBase;
+use Drupal\xray_audit\Plugin\XrayAuditTaskPluginInterface;
+
+/**
+ * Plugin implementation for external resources needing review.
+ *
+ * @XrayAuditInsightPlugin (
+ *   id = "external_resources_review",
+ *   label = @Translation("External resources needing review"),
+ *   description = @Translation("Identified external resources that should be reviewed"),
+ *   sort = 7
+ * )
+ */
+class XrayAuditExternalResourcesInsight extends XrayAuditInsightPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $taskPluginId = 'external_resources';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $operation = 'external_resources';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInsightsForDrupalReport(): array {
+    $insights = $this->getInsights();
+    $needs_review = $insights['external_resources_review'];
+    if ($needs_review) {
+      $url = Url::fromUserInput($this->getPathReport($this->taskPluginId, $this->operation));
+      $reports_link = Link::fromTextAndUrl($this->t('task report'), $url);
+      $value = $this->t('External resources should be reviewed. Please check the @report_link for details', [
+        '@report_link' => $reports_link->toString(),
+      ]);
+      $severity = REQUIREMENT_WARNING;
+    }
+    else {
+      $value = $this->t("No external resources need review.");
+      $severity = NULL;
+    }
+
+    return [
+      'external_resources_review' => $this->buildInsightForDrupalReport(
+        $this->label(),
+        $value,
+        '',
+        $severity
+      ),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInsights(): array {
+    return ['external_resources_review' => $this->existResourcesNeedingReview()];
+  }
+
+  /**
+   * Check if there are external resources needing review.
+   *
+   * @return bool
+   *   True if there are resources needing review.
+   */
+  protected function existResourcesNeedingReview(): bool {
+    $task_plugin = $this->getInstancedPlugin($this->taskPluginId, $this->operation);
+    $result = $task_plugin instanceof XrayAuditTaskPluginInterface ? $task_plugin->getDataOperationResult($this->operation) : [];
+    foreach ($result as $resource) {
+      if (!empty($resource->needs_review)) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+}
diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index 314422e..816278c 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -59,6 +59,13 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
    */
   protected $urlResolver;
 
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
   /**
    * {@inheritdoc}
    */
@@ -71,6 +78,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
     $instance->entityTypeManager = $container->get('entity_type.manager');
     $instance->entityFieldManager = $container->get('entity_field.manager');
     $instance->moduleHandler = $container->get('module_handler');
+    $instance->database = $container->get('database');
 
     if ($instance->moduleHandler->moduleExists('media')) {
       $instance->urlResolver = $container->get('media.oembed.url_resolver');
@@ -119,6 +127,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
     }
 
+    $normalized_urls = [];
     // Process each bundle that has oembed fields.
     foreach ($oembed_fields as $bundle_id => $field_names) {
       // Get all media entities of this bundle.
@@ -163,6 +172,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
                     ->fields('xai')
                     ->condition('type', 'external_resource')
                     ->condition('data', $normalized_url);
+                  $normalized_urls[] = $normalized_url;
                   $insights = $query->execute()->fetchAll();
 
                   if (!empty($insights)) {
@@ -180,9 +190,31 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
     }
 
+    $this->cleanupOrphanedInsights($normalized_urls);
+
     return $results;
   }
 
+  /**
+   * Cleans up orphaned external resource insights that don't match any media.
+   */
+  protected function cleanupOrphanedInsights(array $normalized_urls) {
+    $query = $this->database->select('xray_audit_insight', 'xai')
+      ->fields('xai', ['id', 'data'])
+      ->condition('type', 'external_resource');
+    $insights = $query->execute()->fetchAll();
+
+    foreach ($insights as $insight) {
+      $normalized_url_insight = $insight->data;
+
+      if (!in_array($normalized_url_insight, $normalized_urls)) {
+        $this->database->delete('xray_audit_insight')
+          ->condition('id', $insight->id)
+          ->execute();
+      }
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
-- 
GitLab


From 9aa25a01e7c365e538ee87883c6f7180fea89393 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 14:34:43 +0200
Subject: [PATCH 04/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../src/OEmbed/ResourceFetcherDecorator.php               | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php b/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
index 9002117..d6ea11f 100644
--- a/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
+++ b/modules/xray_audit_insight/src/OEmbed/ResourceFetcherDecorator.php
@@ -14,10 +14,10 @@ class ResourceFetcherDecorator implements ResourceFetcherInterface {
   /**
    * Constructs a new ResourceFetcherDecorator.
    *
-   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
-   *   The original resource fetcher.
-   * @param \Drupal\xray_audit_insight\XrayAuditInsightReport $insight_report
-   *   The XrayAuditInsight report service.
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resourceFetcher
+   *   The original resource fetcher service.
+   * @param \Drupal\xray_audit_insight\XrayAuditInsightReport $insightReport
+   *   The XrayAuditInsightReport service for logging insights.
    */
   public function __construct(
     protected ResourceFetcherInterface $resourceFetcher,
-- 
GitLab


From 4664b2ed75b6979255e9025a3c97403c8ed66d0c Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 14:54:12 +0200
Subject: [PATCH 05/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../src/XrayAuditInsightReport.php            | 55 +++++++++++++------
 .../XrayAuditExternalResourcesPlugin.php      | 40 +++-----------
 2 files changed, 47 insertions(+), 48 deletions(-)

diff --git a/modules/xray_audit_insight/src/XrayAuditInsightReport.php b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
index d48462a..ac1d6e8 100644
--- a/modules/xray_audit_insight/src/XrayAuditInsightReport.php
+++ b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
@@ -26,23 +26,6 @@ class XrayAuditInsightReport {
     $this->database = $database;
   }
 
-  /**
-   * Retrieves insights by type.
-   *
-   * @param string $type
-   *   The type of insights to retrieve.
-   *
-   * @return array
-   *   An array of insights.
-   */
-  public function getInsightsByType($type) {
-    $query = $this->database->select('xray_audit_insight', 'xai')
-      ->fields('xai')
-      ->condition('type', $type);
-
-    return $query->execute()->fetchAll();
-  }
-
   /**
    * Adds a new insight with text data to the xray_audit_insight table.
    *
@@ -86,4 +69,42 @@ class XrayAuditInsightReport {
     return FALSE;
   }
 
+  /**
+   * Gets insights for a specific external resource URL.
+   *
+   * @param string $normalized_url
+   *   The normalized URL to check.
+   *
+   * @return array
+   *   Array of matching insights.
+   */
+  public function getExternalResourceInsights(string $normalized_url) {
+    $query = $this->database->select('xray_audit_insight', 'xai')
+      ->fields('xai')
+      ->condition('type', 'external_resource')
+      ->condition('data', $normalized_url);
+    return $query->execute()->fetchAll();
+  }
+
+  /**
+   * Cleans up orphaned external resource insights that don't match any media.
+   *
+   * @param array $normalized_urls
+   *   Array of active normalized URLs.
+   */
+  public function cleanupOrphanedExternalResourceInsights(array $normalized_urls) {
+    $query = $this->database->select('xray_audit_insight', 'xai')
+      ->fields('xai', ['id', 'data'])
+      ->condition('type', 'external_resource');
+    $insights = $query->execute()->fetchAll();
+
+    foreach ($insights as $insight) {
+      if (!in_array($insight->data, $normalized_urls)) {
+        $this->database->delete('xray_audit_insight')
+          ->condition('id', $insight->id)
+          ->execute();
+      }
+    }
+  }
+
 }
diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index 816278c..f38c6c2 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -60,11 +60,11 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
   protected $urlResolver;
 
   /**
-   * The database connection.
+   * The link generator service.
    *
-   * @var \Drupal\Core\Database\Connection
+   * @var \Drupal\Core\Utility\LinkGeneratorInterface
    */
-  protected $database;
+  protected $linkGenerator;
 
   /**
    * {@inheritdoc}
@@ -78,7 +78,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
     $instance->entityTypeManager = $container->get('entity_type.manager');
     $instance->entityFieldManager = $container->get('entity_field.manager');
     $instance->moduleHandler = $container->get('module_handler');
-    $instance->database = $container->get('database');
+    $instance->linkGenerator = $container->get('link_generator');
 
     if ($instance->moduleHandler->moduleExists('media')) {
       $instance->urlResolver = $container->get('media.oembed.url_resolver');
@@ -168,12 +168,8 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
                   // Get insights of type 'external_resource' where
                   // data matches the normalized resource URL.
                   $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
-                  $query = \Drupal::database()->select('xray_audit_insight', 'xai')
-                    ->fields('xai')
-                    ->condition('type', 'external_resource')
-                    ->condition('data', $normalized_url);
                   $normalized_urls[] = $normalized_url;
-                  $insights = $query->execute()->fetchAll();
+                  $insights = $this->insightReport->getExternalResourceInsights($normalized_url);
 
                   if (!empty($insights)) {
                     $result['needs_review'] = TRUE;
@@ -190,31 +186,13 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
     }
 
-    $this->cleanupOrphanedInsights($normalized_urls);
+    if ($this->insightReport) {
+      $this->insightReport->cleanupOrphanedExternalResourceInsights($normalized_urls);
+    }
 
     return $results;
   }
 
-  /**
-   * Cleans up orphaned external resource insights that don't match any media.
-   */
-  protected function cleanupOrphanedInsights(array $normalized_urls) {
-    $query = $this->database->select('xray_audit_insight', 'xai')
-      ->fields('xai', ['id', 'data'])
-      ->condition('type', 'external_resource');
-    $insights = $query->execute()->fetchAll();
-
-    foreach ($insights as $insight) {
-      $normalized_url_insight = $insight->data;
-
-      if (!in_array($normalized_url_insight, $normalized_urls)) {
-        $this->database->delete('xray_audit_insight')
-          ->condition('id', $insight->id)
-          ->execute();
-      }
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -228,7 +206,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
         'entity_type' => $row->entity_type,
         'bundle' => $row->bundle,
         'resource' => $row->resource,
-        'edit_link' => \Drupal::linkGenerator()->generate($this->t('Edit'), $row->edit_url),
+        'edit_link' => $this->linkGenerator->generate($this->t('Edit'), $row->edit_url),
       ];
 
       // Add insight columns if xray_audit_insight module is enabled.
-- 
GitLab


From 967e0abaa363f4ccf08b852fc94f011bd2d677de Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 15:25:55 +0200
Subject: [PATCH 06/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../XrayAuditExternalResourcesPlugin.php      | 25 +++++++++++++++----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index f38c6c2..929b963 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -127,7 +127,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
     }
 
-    $normalized_urls = [];
+    $media_urls = [];
     // Process each bundle that has oembed fields.
     foreach ($oembed_fields as $bundle_id => $field_names) {
       // Get all media entities of this bundle.
@@ -167,9 +167,24 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
                 if ($this->insightReport) {
                   // Get insights of type 'external_resource' where
                   // data matches the normalized resource URL.
-                  $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
-                  $normalized_urls[] = $normalized_url;
-                  $insights = $this->insightReport->getExternalResourceInsights($normalized_url);
+                  $media_urls[] = $resource_url;
+                  try {
+                    $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
+                    $media_urls[] = $normalized_url;
+                    $insights = $this->insightReport->getExternalResourceInsights($normalized_url);
+                  }
+                  catch (\Exception $e) {
+                    // Log the exception to the xray_audit_insight table.
+                    $this->insightReport->addInsightData(
+                      'external_resource',
+                      $e->getMessage(),
+                      $resource_url
+                    );
+
+                    $result['needs_review'] = TRUE;
+                    // Use the message from the most recent insight.
+                    $result['insight_message'] = $e->getMessage();
+                  }
 
                   if (!empty($insights)) {
                     $result['needs_review'] = TRUE;
@@ -187,7 +202,7 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
     }
 
     if ($this->insightReport) {
-      $this->insightReport->cleanupOrphanedExternalResourceInsights($normalized_urls);
+      $this->insightReport->cleanupOrphanedExternalResourceInsights($media_urls);
     }
 
     return $results;
-- 
GitLab


From 983f712afed63a91118fcf0cdc8970bdf615280a Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Wed, 2 Apr 2025 16:03:03 +0200
Subject: [PATCH 07/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../XrayAuditExternalResourcesPlugin.php      | 60 ++++++++++++-------
 1 file changed, 40 insertions(+), 20 deletions(-)

diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index 929b963..b63a214 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\xray_audit\Plugin\xray_audit\tasks\ContentDisplay;
 
+use Drupal\Component\Utility\Html;
 use Drupal\xray_audit\Plugin\XrayAuditTaskPluginBase;
+use Drupal\xray_audit\Utils\XrayAuditTableFilter;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -215,45 +217,63 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
     $build = [];
 
     $rows = [];
+
+    $headers = [
+      $this->t('Entity ID'),
+      $this->t('Entity Type'),
+      $this->t('Bundle'),
+      $this->t('Resource URL'),
+      $this->t('Edit'),
+    ];
+
     foreach ($data as $row) {
+      $highlight = FALSE;
       $row_data = [
-        'entity_id' => $row->entity_id,
-        'entity_type' => $row->entity_type,
-        'bundle' => $row->bundle,
-        'resource' => $row->resource,
-        'edit_link' => $this->linkGenerator->generate($this->t('Edit'), $row->edit_url),
+        $row->entity_id ?? '',
+        $row->entity_type ?? '',
+        $row->bundle ?? '',
+        $row->resource ?? '',
+        $this->linkGenerator->generate($this->t('Edit'), $row->edit_url) ?? '',
       ];
 
       // Add insight columns if xray_audit_insight module is enabled.
       if ($this->moduleHandler->moduleExists('xray_audit_insight')) {
-        $row_data['needs_review'] = isset($row->needs_review) && $row->needs_review ? $this->t('Yes') : $this->t('No');
-        $row_data['insight_message'] = $row->insight_message ?? '';
+        $row_data[] = isset($row->needs_review) && $row->needs_review ? $this->t('Yes') : $this->t('No');
+        $row_data[] = $row->insight_message ?? '';
+        $highlight = isset($row->needs_review) && $row->needs_review;
       }
 
-      $rows[] = $row_data;
+      $rows[] = ['data' => $row_data, 'class' => $highlight ? ['xray-audit--highlighted'] : []];
     }
 
-    $header = [
-      'Entity ID',
-      'Entity Type',
-      'Bundle',
-      'Resource URL',
-      'Edit',
-    ];
-
     // Add insight headers if xray_audit_insight module is enabled.
     if ($this->moduleHandler->moduleExists('xray_audit_insight')) {
-      $header[] = $this->t('Needs Review');
-      $header[] = $this->t('Message');
+      $headers[] = $this->t('Needs Review');
+      $headers[] = $this->t('Message');
     }
-
+    $unique_id = Html::getUniqueId('xray-audit-external-resources');
     $build['table'] = [
       '#theme' => 'table',
-      '#header' => $header,
+      '#header' => $headers,
+      '#sticky' => TRUE,
+      '#weight' => 10,
       '#rows' => $rows,
       '#empty' => $this->t('No external resources found'),
+      '#attributes' => [
+        'class' => ['xray-audit__table'],
+        'id' => $unique_id,
+      ],
+      '#attached' => [
+        'library' => [
+          'xray_audit/xray_audit',
+        ],
+      ],
     ];
 
+    $columns_indexes = [0, 1, 2, 3, 4, 5, 6];
+    $build['filter'] = XrayAuditTableFilter::generateRenderableFilterInput($unique_id, $columns_indexes, NULL, $headers);
+    $build['filter']['#weight'] = 6;
+
     return $build;
   }
 
-- 
GitLab


From 66a488fcd3f34636aeb536bf0099a4fa3bec6662 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Thu, 3 Apr 2025 08:17:49 +0200
Subject: [PATCH 08/10] Issue #3482085 by eduardo morales alberti: Remove not
 necesary query

---
 .../src/XrayAuditInsightReport.php                | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/modules/xray_audit_insight/src/XrayAuditInsightReport.php b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
index ac1d6e8..3911e3e 100644
--- a/modules/xray_audit_insight/src/XrayAuditInsightReport.php
+++ b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
@@ -93,18 +93,11 @@ class XrayAuditInsightReport {
    *   Array of active normalized URLs.
    */
   public function cleanupOrphanedExternalResourceInsights(array $normalized_urls) {
-    $query = $this->database->select('xray_audit_insight', 'xai')
-      ->fields('xai', ['id', 'data'])
-      ->condition('type', 'external_resource');
-    $insights = $query->execute()->fetchAll();
 
-    foreach ($insights as $insight) {
-      if (!in_array($insight->data, $normalized_urls)) {
-        $this->database->delete('xray_audit_insight')
-          ->condition('id', $insight->id)
-          ->execute();
-      }
-    }
+    $this->database->delete('xray_audit_insight')
+      ->condition('data', $normalized_urls, 'IN')
+      ->execute();
+
   }
 
 }
-- 
GitLab


From adef209b3fa6fb7bc41f2042e72cbdcdd79d035d Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Thu, 3 Apr 2025 08:19:24 +0200
Subject: [PATCH 09/10] Issue #3482085 by eduardo morales alberti: Solve
 condition

---
 modules/xray_audit_insight/src/XrayAuditInsightReport.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/xray_audit_insight/src/XrayAuditInsightReport.php b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
index 3911e3e..e0cabe9 100644
--- a/modules/xray_audit_insight/src/XrayAuditInsightReport.php
+++ b/modules/xray_audit_insight/src/XrayAuditInsightReport.php
@@ -95,7 +95,7 @@ class XrayAuditInsightReport {
   public function cleanupOrphanedExternalResourceInsights(array $normalized_urls) {
 
     $this->database->delete('xray_audit_insight')
-      ->condition('data', $normalized_urls, 'IN')
+      ->condition('data', $normalized_urls, 'NOT IN')
       ->execute();
 
   }
-- 
GitLab


From 1d1e3d4535e0050970908e937b4aacbd6e70fd40 Mon Sep 17 00:00:00 2001
From: Eduardo Morales <eduardo.morales@metadrop.net>
Date: Thu, 3 Apr 2025 08:41:42 +0200
Subject: [PATCH 10/10] Issue #3482085 by eduardo morales alberti: List medias
 that could not be retrieve

---
 .../XrayAuditExternalResourcesPlugin.php      | 178 +++++++++++-------
 1 file changed, 106 insertions(+), 72 deletions(-)

diff --git a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
index b63a214..adaad2e 100644
--- a/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
+++ b/src/Plugin/xray_audit/tasks/ContentDisplay/XrayAuditExternalResourcesPlugin.php
@@ -98,30 +98,33 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
    * {@inheritdoc}
    */
   public function getDataOperationResult(string $operation = '') {
-    $results = [];
     if (!$this->moduleHandler->moduleExists('media')) {
-      return $results;
+      return [];
     }
-    $media_storage = $this->entityTypeManager->getStorage('media');
-    $display_storage = $this->entityTypeManager->getStorage('entity_view_display');
 
-    // Get all media bundles.
-    $bundles = $this->entityTypeManager->getStorage('media_type')->loadMultiple();
+    $oembed_fields = $this->getOembedFields();
+    $results = $this->processMediaEntities($oembed_fields);
+    $this->cleanupInsights($results);
+
+    return $results;
+  }
 
-    // Track fields using OEmbedFormatter.
+  /**
+   * Gets all OEmbed fields from media bundles.
+   */
+  private function getOembedFields(): array {
     $oembed_fields = [];
+    $bundles = $this->entityTypeManager->getStorage('media_type')->loadMultiple();
+    $display_storage = $this->entityTypeManager->getStorage('entity_view_display');
 
-    // Find all view displays using OEmbedFormatter.
     foreach ($bundles as $bundle_id => $bundle) {
-      // Check all view modes for this bundle.
       $displays = $display_storage->loadByProperties([
         'targetEntityType' => 'media',
         'bundle' => $bundle_id,
       ]);
 
       foreach ($displays as $display) {
-        $components = $display->getComponents();
-        foreach ($components as $field_name => $component) {
+        foreach ($display->getComponents() as $field_name => $component) {
           if (isset($component['type']) && str_contains($component['type'], 'oembed')) {
             $oembed_fields[$bundle_id][$field_name] = $field_name;
           }
@@ -129,10 +132,17 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
     }
 
-    $media_urls = [];
-    // Process each bundle that has oembed fields.
+    return $oembed_fields;
+  }
+
+  /**
+   * Process media entities and collect results.
+   */
+  private function processMediaEntities(array $oembed_fields): array {
+    $results = [];
+    $media_storage = $this->entityTypeManager->getStorage('media');
+
     foreach ($oembed_fields as $bundle_id => $field_names) {
-      // Get all media entities of this bundle.
       $media_ids = $media_storage->getQuery()
         ->condition('bundle', $bundle_id)
         ->accessCheck(FALSE)
@@ -143,71 +153,95 @@ class XrayAuditExternalResourcesPlugin extends XrayAuditTaskPluginBase {
       }
 
       $media_entities = $media_storage->loadMultiple($media_ids);
-
-      // Process each media entity.
       foreach ($media_entities as $media_id => $media) {
-        foreach ($field_names as $field_name) {
-          if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
-            // For oembed fields, the URL is typically in the 'value' property.
-            $field_items = $media->get($field_name);
-
-            foreach ($field_items as $field_item) {
-              if ($field_item->getValue() && isset($field_item->getValue()['value'])) {
-                $resource_url = $field_item->getValue()['value'];
-                $result = [
-                  'entity_id' => $media_id,
-                  'entity_type' => 'media',
-                  'bundle' => $bundle_id,
-                  'resource' => $resource_url,
-                  'edit_url' => $media->toUrl('edit-form'),
-                  'needs_review' => FALSE,
-                  'insight_message' => '',
-                ];
-
-                // Check if xray_audit_insight module is enabled
-                // and query for insights.
-                if ($this->insightReport) {
-                  // Get insights of type 'external_resource' where
-                  // data matches the normalized resource URL.
-                  $media_urls[] = $resource_url;
-                  try {
-                    $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
-                    $media_urls[] = $normalized_url;
-                    $insights = $this->insightReport->getExternalResourceInsights($normalized_url);
-                  }
-                  catch (\Exception $e) {
-                    // Log the exception to the xray_audit_insight table.
-                    $this->insightReport->addInsightData(
-                      'external_resource',
-                      $e->getMessage(),
-                      $resource_url
-                    );
-
-                    $result['needs_review'] = TRUE;
-                    // Use the message from the most recent insight.
-                    $result['insight_message'] = $e->getMessage();
-                  }
-
-                  if (!empty($insights)) {
-                    $result['needs_review'] = TRUE;
-                    // Use the message from the most recent insight.
-                    $result['insight_message'] = end($insights)->message;
-                  }
-                }
-
-                $results[] = (object) $result;
-              }
-            }
-          }
+        $this->processMediaEntity($media, $media_id, $bundle_id, $field_names, $results);
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * Process a single media entity.
+   */
+  private function processMediaEntity($media, $media_id, $bundle_id, array $field_names, array &$results): void {
+    foreach ($field_names as $field_name) {
+      if (!$media->hasField($field_name) || $media->get($field_name)->isEmpty()) {
+        continue;
+      }
+
+      foreach ($media->get($field_name) as $field_item) {
+        $value = $field_item->getValue();
+        if (!isset($value['value'])) {
+          continue;
         }
+
+        $result = $this->createBaseResult($media_id, $bundle_id, $value['value'], $media);
+        $this->addInsightData($result, $value['value']);
+        $results[] = (object) $result;
       }
     }
+  }
+
+  /**
+   * Creates base result array for a media item.
+   */
+  private function createBaseResult($media_id, $bundle_id, $resource_url, $media): array {
+    return [
+      'entity_id' => $media_id,
+      'entity_type' => 'media',
+      'bundle' => $bundle_id,
+      'resource' => $resource_url,
+      'edit_url' => $media->toUrl('edit-form'),
+      'needs_review' => FALSE,
+      'insight_message' => '',
+    ];
+  }
 
-    if ($this->insightReport) {
-      $this->insightReport->cleanupOrphanedExternalResourceInsights($media_urls);
+  /**
+   * Adds insight data to the result.
+   */
+  private function addInsightData(array &$result, string $resource_url): void {
+    if (!$this->insightReport) {
+      return;
     }
 
-    return $results;
+    try {
+      $normalized_url = $this->urlResolver->getResourceUrl($resource_url);
+      $insights = $this->insightReport->getExternalResourceInsights($normalized_url);
+
+      if (!empty($insights)) {
+        $result['needs_review'] = TRUE;
+        $result['insight_message'] = end($insights)->message;
+      }
+    }
+    catch (\Exception $e) {
+      $this->insightReport->addInsightData('external_resource', $e->getMessage(), $resource_url);
+      $result['needs_review'] = TRUE;
+      $result['insight_message'] = $e->getMessage();
+    }
+  }
+
+  /**
+   * Cleanup orphaned insights.
+   */
+  private function cleanupInsights(array $results): void {
+    if (!$this->insightReport) {
+      return;
+    }
+
+    $media_urls = array_reduce($results, function ($urls, $result) {
+      $urls[] = $result->resource;
+      try {
+        $urls[] = $this->urlResolver->getResourceUrl($result->resource);
+      }
+      catch (\Exception) {
+        // Ignore URL resolution errors.
+      }
+      return $urls;
+    }, []);
+
+    $this->insightReport->cleanupOrphanedExternalResourceInsights($media_urls);
   }
 
   /**
-- 
GitLab