From da5ea0aeda0780158c0f39b98dd00c8ba53d4c6e Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Mon, 3 Jul 2023 16:40:34 +1000
Subject: [PATCH] Issue #3057545 by acbramley, hchonov, bbrala, bradjones1,
 larowlan, yogeshmpawar, Leon Kessler, gease, joachim, gabesullice, kfritsche,
 jibran, Wim Leers, Berdir, smustgrave, alexpott, catch:
 ResourceTypeRepository wrongly assumes that all entity reference fields have
 the setting "target_type"

---
 .../Field/FieldType/EntityReferenceItem.php   | 18 ++++-
 .../FieldType/EntityReferenceItemBase.php     | 16 +++++
 .../EntityReferenceItemInterface.php          | 29 +++++++++
 core/modules/jsonapi/src/IncludeResolver.php  | 34 ++++++++--
 .../ResourceType/ResourceTypeRepository.php   | 59 +++++++++++------
 .../jsonapi_test_reference_types.info.yml     |  3 +
 .../jsonapi_test_reference_types.module       | 24 +++++++
 .../FieldType/DeprecatedReferenceItem.php     | 65 +++++++++++++++++++
 .../jsonapi/tests/src/Functional/NodeTest.php | 22 +++++++
 .../ResourceType/RelatedResourceTypesTest.php | 11 ++++
 10 files changed, 252 insertions(+), 29 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemBase.php
 create mode 100644 core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemInterface.php
 create mode 100644 core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.info.yml
 create mode 100644 core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.module
 create mode 100644 core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/src/Plugin/Field/FieldType/DeprecatedReferenceItem.php

diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
index c9b2623233f6..7e5b99511e71 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
@@ -13,7 +13,6 @@
 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldException;
-use Drupal\Core\Field\FieldItemBase;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
 use Drupal\Core\Form\FormStateInterface;
@@ -42,7 +41,7 @@
  *   list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
  * )
  */
-class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
+class EntityReferenceItem extends EntityReferenceItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
 
   /**
    * {@inheritdoc}
@@ -778,4 +777,19 @@ public static function getPreconfiguredOptions() {
     return $options;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function getReferenceableBundles(FieldDefinitionInterface $field_definition): array {
+    $settings = $field_definition->getSettings();
+    $target_type_id = $settings['target_type'];
+    $handler_settings = $settings['handler_settings'];
+
+    $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']);
+    $target_bundles = $has_target_bundles
+      ? $handler_settings['target_bundles']
+      : array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type_id));
+    return [$target_type_id => $target_bundles];
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemBase.php
new file mode 100644
index 000000000000..2178d47bb06e
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemBase.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+
+/**
+ * Base class for field items referencing other entities.
+ *
+ * Any field type that is an entity reference should extend from this class in
+ * order to remain backwards compatible with any changes added in the future
+ * to EntityReferenceItemInterface.
+ */
+abstract class EntityReferenceItemBase extends FieldItemBase implements EntityReferenceItemInterface {
+
+}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemInterface.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemInterface.php
new file mode 100644
index 000000000000..624b71ec9e65
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItemInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+
+/**
+ * Interface definition for field items referencing other entities.
+ *
+ * Field items should extend \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemBase.
+ */
+interface EntityReferenceItemInterface {
+
+  /**
+   * Returns the referenceable entity types and bundles.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition for which to retrieve the referenceable entity
+   *   types and bundles.
+   *
+   * @return array
+   *   An array of referenceable bundles where the array is keyed by the entity
+   *   type ID, with values an array of bundle names. (It is a single-value
+   *   array with the entity type ID if the entity type does not implement
+   *   bundles.)
+   */
+  public static function getReferenceableBundles(FieldDefinitionInterface $field_definition): array;
+
+}
diff --git a/core/modules/jsonapi/src/IncludeResolver.php b/core/modules/jsonapi/src/IncludeResolver.php
index 2aad83386f94..b7d47d557621 100644
--- a/core/modules/jsonapi/src/IncludeResolver.php
+++ b/core/modules/jsonapi/src/IncludeResolver.php
@@ -6,7 +6,8 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface;
+use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
 use Drupal\jsonapi\Access\EntityAccessChecker;
 use Drupal\jsonapi\Context\FieldResolver;
 use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
@@ -137,11 +138,32 @@ protected function resolveIncludeTree(array $include_tree, Data $data, Data $inc
           $includes = IncludedData::merge($includes, new IncludedData([$exception]));
           continue;
         }
-        $target_type = $field_list->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
-        assert(!empty($target_type));
-        foreach ($field_list as $field_item) {
-          assert($field_item instanceof EntityReferenceItem);
-          $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue();
+        if (is_subclass_of($field_list->getItemDefinition()->getClass(), EntityReferenceItemInterface::class)) {
+          foreach ($field_list as $field_item) {
+            if (!($field_item->getDataDefinition()->getPropertyDefinition('entity') instanceof DataReferenceDefinitionInterface)) {
+              continue;
+            }
+
+            if (!($field_item->entity instanceof EntityInterface)) {
+              continue;
+            }
+
+            // Support entity reference fields that don't have the referenced
+            // target type stored in settings.
+            $references[$field_item->entity->getEntityTypeId()][] = $field_item->get($field_item::mainPropertyName())->getValue();
+          }
+        }
+        else {
+          @trigger_error(
+            sprintf('Entity reference field items not implementing %s is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3279140', EntityReferenceItemInterface::class),
+            E_USER_DEPRECATED
+          );
+          $target_type = $field_list->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
+          if (!empty($target_type)) {
+            foreach ($field_list as $field_item) {
+              $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue();
+            }
+          }
         }
       }
       foreach ($references as $target_type => $ids) {
diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
index 2223956a6518..57555f066554 100644
--- a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
+++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Installer\InstallerKernel;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface;
 use Drupal\Core\TypedData\DataReferenceTargetDefinition;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
@@ -439,29 +440,45 @@ protected function calculateRelatableResourceTypes(ResourceType $resource_type,
   protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionInterface $field_definition, array $resource_types) {
     $item_definition = $field_definition->getItemDefinition();
     $entity_type_id = $item_definition->getSetting('target_type');
-    $handler_settings = $item_definition->getSetting('handler_settings');
-    $target_bundles = empty($handler_settings['target_bundles']) ? $this->getAllBundlesForEntityType($entity_type_id) : $handler_settings['target_bundles'];
     $relatable_resource_types = [];
+    $item_class = $item_definition->getClass();
+    if (is_subclass_of($item_class, EntityReferenceItemInterface::class)) {
+      $target_type_bundles = $item_class::getReferenceableBundles($field_definition);
+    }
+    else {
+      @trigger_error(
+        sprintf('Entity reference field items not implementing %s is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3279140', EntityReferenceItemInterface::class),
+        E_USER_DEPRECATED
+      );
+      $handler_settings = $item_definition->getSetting('handler_settings');
 
-    foreach ($target_bundles as $target_bundle) {
-      if ($resource_type = static::lookupResourceType($resource_types, $entity_type_id, $target_bundle)) {
-        $relatable_resource_types[] = $resource_type;
-      }
-      // Do not warn during the site installation since system integrity
-      // is not guaranteed in this period and the warnings may pop up falsy,
-      // adding confusion to the process.
-      elseif (!InstallerKernel::installationAttempted()) {
-        trigger_error(
-          sprintf(
-            'The "%s" at "%s:%s" references the "%s:%s" entity type that does not exist. Please take action.',
-            $field_definition->getName(),
-            $field_definition->getTargetEntityTypeId(),
-            $field_definition->getTargetBundle(),
-            $entity_type_id,
-            $target_bundle
-          ),
-          E_USER_WARNING
-        );
+      $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']);
+      $target_bundles = $has_target_bundles ? $handler_settings['target_bundles'] : $this->getAllBundlesForEntityType($entity_type_id);
+      $target_type_bundles = [$entity_type_id => $target_bundles];
+    }
+
+    foreach ($target_type_bundles as $entity_type_id => $target_bundles) {
+      foreach ($target_bundles as $target_bundle) {
+        if ($resource_type = static::lookupResourceType($resource_types, $entity_type_id, $target_bundle)) {
+          $relatable_resource_types[] = $resource_type;
+          continue;
+        }
+        // Do not warn during site installation since system integrity
+        // is not guaranteed during this period and may cause confusing and
+        // unnecessary warnings.
+        if (!InstallerKernel::installationAttempted()) {
+          trigger_error(
+            sprintf(
+              'The "%s" at "%s:%s" references the "%s:%s" entity type that does not exist. Please take action.',
+              $field_definition->getName(),
+              $field_definition->getTargetEntityTypeId(),
+              $field_definition->getTargetBundle(),
+              $entity_type_id,
+              $target_bundle
+            ),
+            E_USER_WARNING
+          );
+        }
       }
     }
 
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.info.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.info.yml
new file mode 100644
index 000000000000..c87903c1da50
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.info.yml
@@ -0,0 +1,3 @@
+name: 'JSON API test deprecated reference field types'
+type: module
+package: Testing
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.module b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.module
new file mode 100644
index 000000000000..df448396837b
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/jsonapi_test_reference_types.module
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains hook implementations for the jsonapi_test_reference_types module.
+ */
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function jsonapi_test_reference_types_entity_base_field_info(EntityTypeInterface $entity_type) {
+  // Add a field of the deprecated reference type to nodes.
+  if ($entity_type->id() === 'node') {
+    $fields = [];
+    $fields['deprecated_reference'] = BaseFieldDefinition::create('jsonapi_test_deprecated_reference')
+      ->setLabel(t('Reference'))
+      ->setDescription(t('Deprecated reference field.'));
+
+    return $fields;
+  }
+}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/src/Plugin/Field/FieldType/DeprecatedReferenceItem.php b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/src/Plugin/Field/FieldType/DeprecatedReferenceItem.php
new file mode 100644
index 000000000000..7067bc5bd869
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_reference_types/src/Plugin/Field/FieldType/DeprecatedReferenceItem.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\jsonapi_test_reference_types\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataReferenceTargetDefinition;
+
+/**
+ * Entity reference field type which doesn't implement the standard interface.
+ *
+ * This is to test the handling of deprecated fields which do not implement
+ * \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface.
+ *
+ * @see https://www.drupal.org/node/3279140
+ * @see \Drupal\Tests\jsonapi\Kernel\ResourceType\RelatedResourceTypesTest::testGetRelatableResourceTypesFromFieldDefinitionEntityReferenceFieldDeprecated()
+ *
+ * @todo Remove this in Drupal 11 https://www.drupal.org/project/drupal/issues/3353314.
+ *
+ * @FieldType(
+ *   id = "jsonapi_test_deprecated_reference",
+ * )
+ */
+class DeprecatedReferenceItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['target_id'] = DataReferenceTargetDefinition::create('integer')
+      ->setLabel(new TranslatableMarkup('Target ID'))
+      ->setSetting('unsigned', TRUE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    $schema = [
+      'columns' => [
+        'target_id' => [
+          'description' => 'The ID of the target entity.',
+          'type' => 'int',
+          'unsigned' => TRUE,
+        ],
+      ],
+      'indexes' => [
+        'target_id' => ['target_id'],
+      ],
+    ];
+
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function mainPropertyName() {
+    return 'target_id';
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
index ec45b2d0b4ef..9c9f5b16c607 100644
--- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
@@ -526,4 +526,26 @@ public function testCollectionFilterAccess() {
     $this->assertContains('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
   }
 
+  /**
+   * Tests deprecated entity reference items.
+   *
+   * @group legacy
+   */
+  public function testDeprecatedEntityReferenceFieldItem(): void {
+    \Drupal::service('module_installer')->install(['jsonapi_test_reference_types']);
+
+    $this->setUpAuthorization('GET');
+    // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
+    $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]);
+    // $url = $this->entity->toUrl('jsonapi');
+    $query = ['include' => 'deprecated_reference'];
+    $url->setOption('query', $query);
+    $request_options = [];
+    $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
+    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
+
+    $this->expectDeprecation('Entity reference field items not implementing Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3279140');
+    $this->request('GET', $url, $request_options);
+  }
+
 }
diff --git a/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php b/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php
index cc7b0dd858ec..5ab0627e02de 100644
--- a/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php
+++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php
@@ -211,4 +211,15 @@ public function testGetRelatableResourceTypesFromFieldDefinition() {
     }
   }
 
+  /**
+   * Test the deprecation error on entity reference fields.
+   *
+   * @group legacy
+   */
+  public function testGetRelatableResourceTypesFromFieldDefinitionEntityReferenceFieldDeprecated(): void {
+    \Drupal::service('module_installer')->install(['jsonapi_test_reference_types']);
+    $this->expectDeprecation('Entity reference field items not implementing Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItemInterface is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3279140');
+    $this->resourceTypeRepository->all();
+  }
+
 }
-- 
GitLab