diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 2a52b79..4d1a257 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Tags; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\Textfield; use Drupal\Core\Site\Settings; @@ -147,7 +148,7 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte 'handler_settings' => $element['#selection_settings'], ); $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); - $autocreate = (bool) $element['#autocreate']; + $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface; $input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($element['#value']); foreach ($input_values as $input) { @@ -167,13 +168,14 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte // Auto-create item. See an example of how this is handled in // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). $value[] = array( - 'entity' => static::createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']) + 'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']), ); } } // Check that the referenced entities are valid, if needed. - if ($element['#validate_reference'] && !$autocreate && !empty($value)) { + if ($element['#validate_reference'] && !empty($value)) { + // Validate existing entities. $ids = array_reduce($value, function ($return, $item) { if (isset($item['target_id'])) { $return[] = $item['target_id']; @@ -189,6 +191,30 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte } } } + + // Validate newly created entities. + $new_entities = array_reduce($value, function ($return, $item) { + if (isset($item['entity'])) { + $return[] = $item['entity']; + } + return $return; + }); + + if ($new_entities) { + if ($autocreate) { + $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities); + $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities); + } + else { + // If the selection handler does not support referencing newly + // created entities, all of them should be invalidated. + $invalid_new_entities = $new_entities; + } + + foreach ($invalid_new_entities as $entity) { + $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', array('%type' => $element['#target_type'], '%label' => $entity->label()))); + } + } } // Use only the last value if the form element does not support multiple @@ -310,37 +336,4 @@ public static function extractEntityIdFromAutocompleteInput($input) { return $match; } - /** - * Creates a new entity from a label entered in the autocomplete input. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle - * The bundle name. - * @param string $label - * The entity label. - * @param int $uid - * The entity owner ID. - * - * @return \Drupal\Core\Entity\EntityInterface - */ - protected static function createNewEntity($entity_type_id, $bundle, $label, $uid) { - $entity_manager = \Drupal::entityManager(); - - $entity_type = $entity_manager->getDefinition($entity_type_id); - $bundle_key = $entity_type->getKey('bundle'); - $label_key = $entity_type->getKey('label'); - - $entity = $entity_manager->getStorage($entity_type_id)->create(array( - $bundle_key => $bundle, - $label_key => $label, - )); - - if ($entity instanceof EntityOwnerInterface) { - $entity->setOwnerId($uid); - } - - return $entity; - } - } diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php index 83120d1..132b9a7 100644 --- a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php @@ -30,7 +30,7 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0); /** - * Counts entities that are referenceable by a given field. + * Counts entities that are referenceable. * * @return int * The number of referenceable entities. @@ -38,7 +38,7 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS'); /** - * Validates that entities can be referenced by this field. + * Validates which existing entities can be referenced. * * @return array * An array of valid entity IDs. diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php new file mode 100644 index 0000000..7010b91 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php @@ -0,0 +1,52 @@ +entityManager->getDefinition($entity_type_id); + $bundle_key = $entity_type->getKey('bundle'); + $label_key = $entity_type->getKey('label'); + + $entity = $this->entityManager->getStorage($entity_type_id)->create(array( + $bundle_key => $bundle, + $label_key => $label, + )); + + if ($entity instanceof EntityOwnerInterface) { + $entity->setOwnerId($uid); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + return array_filter($entities, function ($entity) { + if (isset($this->configuration['handler_settings']['target_bundles'])) { + return in_array($entity->bundle(), $this->configuration['handler_settings']['target_bundles']); + } + return TRUE; + }); + } + + /** * Builds an EntityQuery to get referenceable entities. * * @param string|null $match diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php index bf8658a..f27c22d 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php @@ -26,10 +26,24 @@ class ValidReferenceConstraint extends Constraint { * * @var string */ - public $message = 'The referenced entity (%type: %id) does not exist.'; + public $message = 'This entity (%type: %id) cannot be referenced.'; /** - * Validation message when the target_id is empty. + * Violation message when the entity does not exist. + * + * @var string + */ + public $nonExistingMessage = 'The referenced entity (%type: %id) does not exist.'; + + /** + * Violation message when a new entity ("autocreate") is invalid. + * + * @var string + */ + public $invalidAutocreateMessage = 'This entity (%type: %label) cannot be referenced.'; + + /** + * Violation message when the target_id is empty. * * @var string */ diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php index 8f704c1..f228bbc 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php @@ -7,39 +7,142 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; /** * Checks if referenced entities are valid. */ -class ValidReferenceConstraintValidator extends ConstraintValidator { +class ValidReferenceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { + + /** + * The selection plugin manager. + * + * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface + */ + protected $selectionManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a ValidReferenceConstraintValidator object. + * + * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager + * The selection plugin manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(SelectionPluginManagerInterface $selection_manager, EntityTypeManagerInterface $entity_type_manager) { + $this->selectionManager = $selection_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.entity_reference_selection'), + $container->get('entity_type.manager') + ); + } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { - /** @var \Drupal\Core\Field\FieldItemInterface $value */ + /** @var \Drupal\Core\Field\FieldItemListInterface $value */ /** @var ValidReferenceConstraint $constraint */ if (!isset($value)) { return; } - // We don't use a regular NotNull constraint for the target_id property as - // a NULL value is valid if the entity property contains an unsaved entity. - // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints - if (!$value->isEmpty() && $value->target_id === NULL && !$value->entity->isNew()) { - $this->context->addViolation($constraint->nullMessage); - return; + + // Collect new entities and IDs of existing entities across the field items. + $new_entities = []; + $target_ids = []; + foreach ($value as $delta => $item) { + $target_id = $item->target_id; + // We don't use a regular NotNull constraint for the target_id property as + // NULL is allowed if the entity property contains an unsaved entity. + // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints() + if (!$item->isEmpty() && $target_id === NULL) { + if (!$item->entity->isNew()) { + $this->context->buildViolation($constraint->nullMessage) + ->atPath((string) $delta) + ->addViolation(); + return; + } + $new_entities[$delta] = $item->entity; + } + + // '0' or NULL are considered valid empty references. + if (!empty($target_id)) { + $target_ids[$delta] = $target_id; + } } - $id = $value->get('target_id')->getValue(); - // '0' or NULL are considered valid empty references. - if (empty($id)) { + + // Early opt-out if nothing to validate. + if (!$new_entities && !$target_ids) { return; } - $referenced_entity = $value->get('entity')->getValue(); - if (!$referenced_entity) { - $type = $value->getFieldDefinition()->getSetting('target_type'); - $this->context->addViolation($constraint->message, array('%type' => $type, '%id' => $id)); + + /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler * */ + $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition()); + $target_type_id = $value->getFieldDefinition()->getSetting('target_type'); + + // Add violations on deltas with a new entity that is not valid. + if ($new_entities) { + if ($handler instanceof SelectionWithAutocreateInterface) { + $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities); + $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities); + } + else { + // If the selection handler does not support referencing newly created + // entities, all of them should be invalidated. + $invalid_new_entities = $new_entities; + } + + foreach ($invalid_new_entities as $delta => $entity) { + $this->context->buildViolation($constraint->invalidAutocreateMessage) + ->setParameter('%type', $target_type_id) + ->setParameter('%label', $entity->label()) + ->atPath((string) $delta . '.entity') + ->setInvalidValue($entity) + ->addViolation(); + } + } + + // Add violations on deltas with a target_id that is not valid. + if ($target_ids) { + $valid_target_ids = $handler->validateReferenceableEntities($target_ids); + if ($invalid_target_ids = array_diff($target_ids, $valid_target_ids)) { + // For accuracy of the error message, differentiate non-referenceable + // and non-existent entities. + $target_type = $this->entityTypeManager->getDefinition($target_type_id); + $existing_ids = $this->entityTypeManager->getStorage($target_type_id)->getQuery() + ->condition($target_type->getKey('id'), $invalid_target_ids, 'IN') + ->execute(); + foreach ($invalid_target_ids as $delta => $target_id) { + $message = in_array($target_id, $existing_ids) ? $constraint->message : $constraint->nonExistingMessage; + $this->context->buildViolation($message) + ->setParameter('%type', $target_type_id) + ->setParameter('%id', $target_id) + ->atPath((string) $delta . '.target_id') + ->setInvalidValue($target_id) + ->addViolation(); + } + } } } + } diff --git a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php index 4217b2e..cd221d2 100644 --- a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php +++ b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php @@ -18,6 +18,16 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere /** * {@inheritdoc} */ + public function getConstraints() { + $constraints = parent::getConstraints(); + $constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager(); + $constraints[] = $constraint_manager->create('ValidReference', []); + return $constraints; + } + + /** + * {@inheritdoc} + */ public function referencedEntities() { if (empty($this->list)) { return array(); 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 d7234de..9ad1a9d 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -40,7 +40,6 @@ * default_widget = "entity_reference_autocomplete", * default_formatter = "entity_reference_label", * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList", - * constraints = {"ValidReference" = {}} * ) */ class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface { @@ -163,20 +162,6 @@ public function getConstraints() { unset($constraints[$key]); } } - list($current_handler) = explode(':', $this->getSetting('handler'), 2); - if ($current_handler === 'default') { - $handler_settings = $this->getSetting('handler_settings'); - if (isset($handler_settings['target_bundles'])) { - $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager(); - $constraints[] = $constraint_manager->create('ComplexData', [ - 'entity' => [ - 'Bundle' => [ - 'bundle' => $handler_settings['target_bundles'], - ], - ], - ]); - } - } return $constraints; } diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php index c8bc4a7..b053170 100644 --- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php +++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php @@ -42,6 +42,34 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') /** * {@inheritdoc} */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $comment = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable comment, it needs to published. + /** @var \Drupal\comment\CommentInterface $comment */ + $comment->setPublished(TRUE); + + return $comment; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('administer comments')) { + $entities = array_filter($entities, function ($comment) { + /** @var \Drupal\comment\CommentInterface $comment */ + return $comment->isPublished(); + }); + } + return $entities; + } + + /** + * {@inheritdoc} + */ public function entityQueryAlter(SelectInterface $query) { $tables = $query->getTables(); $data_table = 'comment_field_data'; diff --git a/core/modules/comment/src/Tests/CommentValidationTest.php b/core/modules/comment/src/Tests/CommentValidationTest.php index 62acec0..5c30d89 100644 --- a/core/modules/comment/src/Tests/CommentValidationTest.php +++ b/core/modules/comment/src/Tests/CommentValidationTest.php @@ -39,7 +39,7 @@ protected function setUp() { */ public function testValidation() { // Add a user. - $user = User::create(array('name' => 'test')); + $user = User::create(array('name' => 'test', 'status' => TRUE)); $user->save(); // Add comment type. diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php index f8a9348..f629ce8 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php @@ -7,6 +7,8 @@ namespace Drupal\field\Tests\EntityReference; +use Drupal\comment\Entity\Comment; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; @@ -17,8 +19,11 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Tests\FieldUnitTestBase; +use Drupal\file\Entity\File; +use Drupal\node\Entity\Node; use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\user\Entity\User; /** @@ -35,7 +40,7 @@ class EntityReferenceItemTest extends FieldUnitTestBase { * * @var array */ - public static $modules = ['taxonomy', 'text', 'filter', 'views', 'field']; + public static $modules = ['node', 'comment', 'file', 'taxonomy', 'text', 'filter', 'views', 'field']; /** * The taxonomy vocabulary to test with. @@ -66,6 +71,11 @@ protected function setUp() { $this->installEntitySchema('entity_test_string_id'); $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + + $this->installSchema('comment', ['comment_entity_statistics']); $this->vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomMachineName(), @@ -90,6 +100,10 @@ protected function setUp() { $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_entity_test_string_id', 'Test content entity reference with string ID', 'entity_test_string_id'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_node', 'Test node entity reference', 'node'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_user', 'Test user entity reference', 'user'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_comment', 'Test comment entity reference', 'comment'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_file', 'Test file entity reference', 'file'); } /** @@ -333,9 +347,9 @@ public function testSelectionHandlerSettings() { } /** - * Tests validation constraint. + * Tests ValidReferenceConstraint with newly created and unsaved entities. */ - public function testValidation() { + public function testAutocreateValidation() { // The term entity is unsaved here. $term = Term::create(array( 'name' => $this->randomMachineName(), @@ -367,6 +381,100 @@ public function testValidation() { $entity->save(); $errors = $entity->validate(); $this->assertEqual(0, count($errors)); + + // Test with an unpublished and unsaved node. + $title = $this->randomString(); + $node = Node::create([ + 'title' => $title, + 'type' => 'node', + 'status' => NODE_NOT_PUBLISHED, + ]); + + $entity = EntityTest::create([ + 'field_test_node' => [ + 'entity' => $node, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_node.0.entity'); + + // Publish the node and try again. + $node->setPublished(TRUE); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with an unpublished and unsaved comment. + $title = $this->randomString(); + $comment = Comment::create([ + 'subject' => $title, + 'comment_type' => 'comment', + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_comment' => [ + 'entity' => $comment, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'comment', '%label' => $title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_comment.0.entity'); + + // Publish the comment and try again. + $comment->setPublished(TRUE); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with an inactive and unsaved user. + $name = $this->randomString(); + $user = User::create([ + 'name' => $name, + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_user' => [ + 'entity' => $user, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'user', '%label' => $name])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_user.0.entity'); + + // Activate the user and try again. + $user->activate(); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with a temporary and unsaved file. + $filename = $this->randomMachineName() . '.txt'; + $file = File::create([ + 'filename' => $filename, + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_file' => [ + 'entity' => $file, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'file', '%label' => $filename])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_file.0.entity'); + + // Set the file as permanent and try again. + $file->setPermanent(); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); } } diff --git a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php index e789154..01fb8cd 100644 --- a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php +++ b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php @@ -27,8 +27,41 @@ class FileSelection extends DefaultSelection { */ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { $query = parent::buildEntityQuery($match, $match_operator); - $query->condition('status', FILE_STATUS_PERMANENT); + // Allow referencing : + // - files with status "permanent" + // - or files uploaded by the current user (since newly uploaded files only + // become "permanent" after the containing entity gets validated and + // saved.) + $query->condition($query->orConditionGroup() + ->condition('status', FILE_STATUS_PERMANENT) + ->condition('uid', $this->currentUser->id())); return $query; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $file = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable file, it needs to have a "permanent" + // status. + /** @var \Drupal\file\FileInterface $file */ + $file->setPermanent(); + + return $file; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + $entities = array_filter($entities, function ($file) { + /** @var \Drupal\file\FileInterface $file */ + return $file->isPermanent() || $file->getOwnerId() === $this->currentUser->id(); + }); + return $entities; + } + } diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index 65f4f39..ba50903 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -28,7 +28,7 @@ * default_widget = "file_generic", * default_formatter = "file_default", * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", - * constraints = {"ValidReference" = {}, "ReferenceAccess" = {}, "FileValidation" = {}} + * constraints = {"ReferenceAccess" = {}, "FileValidation" = {}} * ) */ class FileItem extends EntityReferenceItem { diff --git a/core/modules/file/tests/src/Kernel/FileItemValidationTest.php b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php index cc83b23..fbfcbd1 100644 --- a/core/modules/file/tests/src/Kernel/FileItemValidationTest.php +++ b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php @@ -47,6 +47,7 @@ protected function setUp() { $this->user = User::create([ 'name' => 'username', + 'status' => 1, ]); $this->user->save(); } @@ -90,6 +91,7 @@ public function testFileValidationConstraint($file_type) { $file = File::create([ 'uri' => 'vfs://drupal_root/sites/default/files/test.txt', ]); + $file->setPermanent(); $file->save(); $entity_test = EntityTest::create([ diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 6f95693..9ec3b67 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -43,7 +43,7 @@ * }, * }, * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", - * constraints = {"ValidReference" = {}, "ReferenceAccess" = {}, "FileValidation" = {}} + * constraints = {"ReferenceAccess" = {}, "FileValidation" = {}} * ) */ class ImageItem extends FileItem { @@ -346,7 +346,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin if ($path = $random->image(drupal_realpath($destination), $min_resolution, $max_resolution)) { $image = File::create(); $image->setFileUri($path); - // $image->setOwner($account); + $image->setOwnerId(\Drupal::currentUser()->id()); $image->setMimeType('image/' . pathinfo($path, PATHINFO_EXTENSION)); $image->setFileName(drupal_basename($path)); $destination_dir = static::doGetUploadLocation($settings); diff --git a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php index 276c795..a42fa60 100644 --- a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php +++ b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php @@ -48,4 +48,32 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') return $query; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $node = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable node, it needs to published. + /** @var \Drupal\node\NodeInterface $node */ + $node->setPublished(TRUE); + + return $node; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) { + $entities = array_filter($entities, function ($node) { + /** @var \Drupal\node\NodeInterface $node */ + return $node->isPublished(); + }); + } + return $entities; + } + } diff --git a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php index 0fdd9af..3e0386c 100644 --- a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php +++ b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php @@ -137,6 +137,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['single_autocreate_no_validate'] = array( '#type' => 'entity_autocomplete', '#target_type' => 'entity_test', + '#validate_reference' => FALSE, '#autocreate' => array( 'bundle' => 'entity_test', ), diff --git a/core/modules/system/src/Tests/Entity/EntityFieldTest.php b/core/modules/system/src/Tests/Entity/EntityFieldTest.php index 6fae79b..5c9161e 100644 --- a/core/modules/system/src/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php @@ -691,8 +691,8 @@ public function testEntityConstraintValidation() { ->setSetting('target_type', 'node') ->setSetting('handler_settings', ['target_bundles' => ['article' => 'article']]); $reference_field = \Drupal::TypedDataManager()->create($definition); - $reference = $reference_field->appendItem(array('entity' => $node)); - $violations = $reference->validate(); + $reference_field->appendItem(array('entity' => $node)); + $violations = $reference_field->validate(); $this->assertEqual($violations->count(), 1); $node = entity_create('node', array( @@ -701,8 +701,8 @@ public function testEntityConstraintValidation() { 'title' => $this->randomString(), )); $node->save(); - $reference->setValue($node); - $violations = $reference->validate(); + $reference_field->entity = $node; + $violations = $reference_field->validate(); $this->assertEqual($violations->count(), 0); } diff --git a/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php b/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php index 8e64386..aa49b84 100644 --- a/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php @@ -113,7 +113,7 @@ public function testEntityReferenceFieldValidation() { $entity->{$this->fieldName}->target_id = $referenced_entity->id(); $violations = $entity->{$this->fieldName}->validate(); $this->assertEqual($violations->count(), 1, 'Validation throws a violation.'); - $this->assertEqual($violations[0]->getMessage(), t('The entity must be of bundle %bundle.', array('%bundle' => $this->bundle))); + $this->assertEqual($violations[0]->getMessage(), t('This entity (%type: %id) cannot be referenced.', array('%type' => $this->referencedEntityType, '%id' => $referenced_entity->id()))); } /** diff --git a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php index 160c50e..3964f29 100644 --- a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php +++ b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php @@ -171,6 +171,42 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') /** * {@inheritdoc} */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable user, it needs to be active. + if (!$this->currentUser->hasPermission('administer users')) { + /** @var \Drupal\user\UserInterface $user */ + $user->activate(); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!empty($this->configuration['handler_settings']['filter']['role'])) { + $entities = array_filter($entities, function ($user) { + /** @var \Drupal\user\UserInterface $user */ + return !empty(array_intersect($user->getRoles(), $this->configuration['handler_settings']['filter']['role'])); + }); + } + if (!$this->currentUser->hasPermission('administer users')) { + $entities = array_filter($entities, function ($user) { + /** @var \Drupal\user\UserInterface $user */ + return $user->isActive(); + }); + } + return $entities; + } + + /** + * {@inheritdoc} + */ public function entityQueryAlter(SelectInterface $query) { // Bail out early if we do not need to match the Anonymous user. $handler_settings = $this->configuration['handler_settings']; diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php index 4891d5a..dfa8a21 100644 --- a/core/modules/user/src/Tests/UserValidationTest.php +++ b/core/modules/user/src/Tests/UserValidationTest.php @@ -176,7 +176,7 @@ function testValidation() { $user->roles[1]->target_id = 'unknown_role'; $violations = $user->validate(); $this->assertEqual(count($violations), 1); - $this->assertEqual($violations[0]->getPropertyPath(), 'roles.1'); + $this->assertEqual($violations[0]->getPropertyPath(), 'roles.1.target_id'); $this->assertEqual($violations[0]->getMessage(), t('The referenced entity (%entity_type: %name) does not exist.', array('%entity_type' => 'user_role', '%name' => 'unknown_role'))); }