diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 73db361..5db50dd 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -798,6 +798,9 @@ entity_reference_selection: auto_create_bundle: type: string label: 'Bundle assigned to the auto-created entities.' + allow_self_reference: + type: boolean + label: 'Allow an entity to reference itself' entity_reference_selection.*: type: entity_reference_selection diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 51adf91..fc3938a 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -32,6 +32,7 @@ public function getInfo() { $info['#target_type'] = NULL; $info['#selection_handler'] = 'default'; $info['#selection_settings'] = array(); + $info['#host_entity_id'] = NULL; $info['#tags'] = FALSE; $info['#autocreate'] = NULL; // This should only be set to FALSE if proper validation by the selection @@ -140,6 +141,7 @@ public static function processEntityAutocomplete(array &$element, FormStateInter 'target_type' => $element['#target_type'], 'selection_handler' => $element['#selection_handler'], 'selection_settings_key' => $selection_settings_key, + 'host_entity_id' => $element['#host_entity_id'], ); return $element; diff --git a/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php index 01a29b2..e0b108c 100644 --- a/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php +++ b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php @@ -39,6 +39,8 @@ public function __construct(SelectionPluginManagerInterface $selection_manager) * An array of settings that will be passed to the selection handler. * @param string $string * (optional) The label of the entity to query by. + * @param \Drupal\Core\Entity\EntityInterface $host_entity + * (optional) The referencing entity, if any. Defaults to NULL. * * @return array * An array of matched entity labels, in the format required by the AJAX @@ -49,13 +51,14 @@ public function __construct(SelectionPluginManagerInterface $selection_manager) * * @see \Drupal\system\Controller\EntityAutocompleteController */ - public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') { + public function getMatches($target_type, $selection_handler, $selection_settings, $string = '', EntityInterface $host_entity = NULL) { $matches = array(); $options = array( 'target_type' => $target_type, 'handler' => $selection_handler, 'handler_settings' => $selection_settings, + 'entity' => $host_entity, ); $handler = $this->selectionManager->getInstance($options); diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php index 9565e77..caa1cc8 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php @@ -118,6 +118,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ), 'auto_create' => FALSE, 'auto_create_bundle' => NULL, + 'allow_self_reference' => TRUE, ); if ($entity_type->hasKey('bundle')) { @@ -240,6 +241,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ]; } + $form['allow_self_reference'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Allow an entity to reference itself'), + '#default_value' => $selection_handler_settings['allow_self_reference'], + ); + return $form; } @@ -397,6 +404,11 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') $query->condition($label_key, $match, $match_operator); } + // Disallow self-references if possible. + if (isset($this->configuration['entity']) && isset($handler_settings['allow_self_reference']) && !$handler_settings['allow_self_reference']) { + $query->condition($entity_type->getKey('id'), $this->configuration['entity']->id(), '<>'); + } + // Add entity-access tag. $query->addTag($target_type . '_access'); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php index b5e16c1..151cbbb 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php @@ -108,6 +108,11 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen ); } + // Pass the ID of the referencing entity if it is available. + if (!$entity->isNew()) { + $element['#host_entity_id'] = $entity->id(); + } + return array('target_id' => $element); } diff --git a/core/modules/system/src/Controller/EntityAutocompleteController.php b/core/modules/system/src/Controller/EntityAutocompleteController.php index 53ec818..aa59b77 100644 --- a/core/modules/system/src/Controller/EntityAutocompleteController.php +++ b/core/modules/system/src/Controller/EntityAutocompleteController.php @@ -68,6 +68,8 @@ public static function create(ContainerInterface $container) { * @param string $selection_settings_key * The hashed key of the key/value entry that holds the selection handler * settings. + * @param string $host_entity_id + * (optional) The ID of the referencing entity, if any. Defaults to none. * * @return \Symfony\Component\HttpFoundation\JsonResponse * The matched entity labels as a JSON response. @@ -76,8 +78,10 @@ public static function create(ContainerInterface $container) { * Thrown if the selection settings key is not found in the key/value store * or if it does not match the stored data. */ - public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings_key) { + public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings_key, $host_entity_id = '') { $matches = array(); + $host_entity = NULL; + // Get the typed string from the URL, if it exists. if ($input = $request->query->get('q')) { $typed_string = Tags::explode($input); @@ -100,7 +104,14 @@ public function handleAutocomplete(Request $request, $target_type, $selection_ha throw new AccessDeniedHttpException(); } - $matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string); + if ($host_entity_id) { + $host_entity = $this->entityTypeManager()->getStorage($target_type)->load($host_entity_id); + if (!$host_entity || !$host_entity->access('view')) { + throw new AccessDeniedHttpException(); + } + } + + $matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string, $host_entity); } return new JsonResponse($matches); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index ae89fb0..6f447a2 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -495,9 +495,10 @@ system.admin_content: _permission: 'access administration pages' system.entity_autocomplete: - path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings_key}' + path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings_key}/{host_entity_id}' defaults: _controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete' + host_entity_id: '' requirements: _access: 'TRUE' diff --git a/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php b/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php index f7fa5d8..fc0b5f6 100644 --- a/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php +++ b/core/modules/system/tests/src/Kernel/Entity/EntityReferenceSelectionReferenceableTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; +use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; use Drupal\field\Entity\FieldConfig; use Drupal\node\Entity\NodeType; @@ -50,6 +51,7 @@ class EntityReferenceSelectionReferenceableTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installEntitySchema('entity_test'); $this->installEntitySchema('entity_test_no_label'); /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ @@ -83,6 +85,32 @@ protected function setUp() { } /** + * Tests the 'allow_self_reference' selection handler setting. + */ + public function testAllowSelfReference() { + $field_name = 'field_test'; + $this->createEntityReferenceField('entity_test', 'entity_test', $field_name, 'Test entity reference', 'entity_test'); + + // Create a test entity and check that it cannot reference itself. + $host_entity = EntityTest::create(['name' => $this->randomMachineName()]); + $host_entity->save(); + + $field_config = FieldConfig::loadByName('entity_test', 'entity_test', $field_name); + $this->selectionHandler = $this->container->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_config, $host_entity); + + $referenceables = $this->selectionHandler->getReferenceableEntities(); + $this->assertTrue(isset($referenceables['entity_test'][$host_entity->id()])); + + $selection_setttings = $field_config->getSetting('handler_settings'); + $selection_setttings['allow_self_reference'] = FALSE; + $field_config->setSetting('handler_settings', $selection_setttings)->save(); + + $this->selectionHandler = $this->container->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_config, $host_entity); + $referenceables = $this->selectionHandler->getReferenceableEntities(); + $this->assertTrue(!isset($referenceables['entity_test'][$host_entity->id()])); + } + + /** * Tests values returned by SelectionInterface::getReferenceableEntities() * when the target entity type has no 'label' key. *