diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index ee3d5d7..93ecfc4 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -222,6 +222,34 @@ field.widget.settings.checkbox: type: boolean label: 'Use field label instead of the "On value" as label' +field.widget.settings.entity_reference_autocomplete_tags: + type: mapping + label: 'Entity reference autocomplete (Tags style) display format settings' + mapping: + match_operator: + type: string + label: 'Autocomplete matching' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' + +field.widget.settings.entity_reference_autocomplete: + type: mapping + label: 'Entity reference autocomplete display format settings' + mapping: + match_operator: + type: string + label: 'Autocomplete matching' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' + field.formatter.settings.boolean: type: mapping mapping: diff --git a/core/core.services.yml b/core/core.services.yml index 29a734a..b972add 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -357,6 +357,9 @@ services: arguments: ['@config.manager', '@entity.manager'] tags: - { name: event_subscriber } + entity.autocomplete_matcher: + class: Drupal\Core\Entity\EntityAutocompleteMatcher + arguments: ['@plugin.manager.entity_reference_selection'] plugin.manager.entity_reference_selection: class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager parent: default_plugin_manager diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php new file mode 100644 index 0000000..bf415af --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -0,0 +1,220 @@ + .., 'value' => ..) structure instead of manually + // composing the textfield string?. See https://www.drupal.org/node/2418249. + + return $info; + } + + /** + * Adds entity autocomplete functionality to a form element. + * + * @param array $element + * The form element to process. Properties used: + * - #target_type: The ID of the target entity type. + * - #selection_handler: The plugin ID of the entity reference selection + * handler. + * - #selection_settings: An array of settings that will be passed to the + * selection handler. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The form element. + * + * @throws \InvalidArgumentException + * Exception thrown when the #target_type or #autocreate['bundle'] are + * missing. + */ + public static function processEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) { + // Nothing to do if there is no target entity type. + if (empty($element['#target_type'])) { + throw new \InvalidArgumentException('Missing required #target_type parameter.'); + } + + // Provide default values and sanity checks for the #autocreate parameter. + if ($element['#autocreate']) { + if (!isset($element['#autocreate']['bundle'])) { + throw new \InvalidArgumentException("Missing required #autocreate['bundle'] parameter."); + } + // Default the autocreate user ID to the current user. + $element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id(); + } + + $element['#autocomplete_route_name'] = 'system.entity_autocomplete'; + $element['#autocomplete_route_parameters'] = array( + 'target_type' => $element['#target_type'], + 'selection_handler' => $element['#selection_handler'], + 'selection_settings' => $element['#selection_settings'] ? base64_encode(serialize($element['#selection_settings'])) : '', + ); + + return $element; + } + + /** + * Form element validation handler for entity_autocomplete elements. + */ + public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) { + $value = NULL; + if (!empty($element['#value'])) { + $options = array( + 'target_type' => $element['#target_type'], + 'handler' => $element['#selection_handler'], + 'handler_settings' => $element['#selection_settings'], + ); + $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); + $autocreate = (bool) $element['#autocreate']; + + foreach (Tags::explode($element['#value']) as $input) { + $match = static::extractEntityIdFromAutocompleteInput($input); + if ($match === NULL) { + // Try to get a match from the input string when the user didn't use + // the autocomplete but filled in a value manually. + $match = $handler->validateAutocompleteInput($input, $element, $form_state, $complete_form, !$autocreate); + } + + if ($match !== NULL) { + $value[] = array( + 'target_id' => $match, + ); + } + elseif ($autocreate) { + // 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']) + ); + } + } + + // Check that the referenced entities are valid, if needed. + if ($element['#validate_reference'] && !$autocreate && !empty($value)) { + $ids = array_reduce($value, function ($return, $item) { + if (isset($item['target_id'])) { + $return[] = $item['target_id']; + } + return $return; + }); + + if ($ids) { + $valid_ids = $handler->validateReferenceableEntities($ids); + if ($invalid_ids = array_diff($ids, $valid_ids)) { + foreach ($invalid_ids as $invalid_id) { + $form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', array('%type' => $element['#target_type'], '%id' => $invalid_id))); + } + } + } + } + + // Use only the last value if the form element does not support multiple + // matches (tags). + if (!$element['#tags'] && !empty($value)) { + $last_value = $value[count($value) - 1]; + $value = isset($last_value['target_id']) ? $last_value['target_id'] : $last_value; + } + } + + $form_state->setValueForElement($element, $value); + } + + /** + * Extracts the entity ID from the autocompletion result. + * + * @param string $input + * The input coming from the autocompletion result. + * + * @return mixed|null + * An entity ID or NULL if the input does not contain one. + */ + public static function extractEntityIdFromAutocompleteInput($input) { + $match = NULL; + + // Take "label (entity id)', match the ID from parenthesis when it's a + // number. + if (preg_match("/.+\((\d+)\)/", $input, $matches)) { + $match = $matches[1]; + } + // Match the ID when it's a string (e.g. for config entity types). + elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) { + $match = $matches[1]; + } + + 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/modules/entity_reference/src/EntityReferenceAutocomplete.php b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php similarity index 38% rename from core/modules/entity_reference/src/EntityReferenceAutocomplete.php rename to core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php index f0a6157..9fec09a 100644 --- a/core/modules/entity_reference/src/EntityReferenceAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php @@ -2,67 +2,46 @@ /** * @file - * Contains \Drupal\entity_reference/EntityReferenceAutocomplete. + * Contains \Drupal\Core\Entity\EntityAutocompleteMatcher. */ -namespace Drupal\entity_reference; +namespace Drupal\Core\Entity; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Tags; -use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** - * Helper class to get autocompletion results for entity reference. + * Matcher class to get autocompletion results for entity reference. */ -class EntityReferenceAutocomplete { +class EntityAutocompleteMatcher { /** - * The entity manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityManager; - - /** - * The Entity reference selection handler plugin manager. + * The entity reference selection handler plugin manager. * * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface */ - protected $selectionHandlerManager; + protected $selectionManager; /** - * Constructs a EntityReferenceAutocomplete object. + * Constructs a EntityAutocompleteMatcher object. * - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager. * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager - * The Entity reference selection handler plugin manager. + * The entity reference selection handler plugin manager. */ - public function __construct(EntityManagerInterface $entity_manager, SelectionPluginManagerInterface $selection_manager) { - $this->entityManager = $entity_manager; - $this->selectionHandlerManager = $selection_manager; + public function __construct(SelectionPluginManagerInterface $selection_manager) { + $this->selectionManager = $selection_manager; } /** * Returns matched labels based on a given search string. * - * This function can be used by other modules that wish to pass a mocked - * definition of the field on instance. - * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The field definition. - * @param string $entity_type - * The entity type. - * @param string $bundle - * The entity bundle. - * @param string $entity_id - * (optional) The entity ID the entity reference field is attached to. - * Defaults to ''. - * @param string $prefix - * (optional) A prefix for all the keys returned by this function. + * @param string $target_type + * The ID of the target entity type. + * @param string $selection_handler + * The plugin ID of the entity reference selection handler. + * @param array $selection_settings + * An array of settings that will be passed to the selection handler. * @param string $string * (optional) The label of the entity to query by. * @@ -70,26 +49,24 @@ public function __construct(EntityManagerInterface $entity_manager, SelectionPlu * Thrown when the current user doesn't have access to the specifies entity. * * @return array - * A list of matched entity labels. + * An array of matched entity labels, in the format required by the AJAX + * autocomplete API (e.g. array('value' => $value, 'label' => $label)). * - * @see \Drupal\entity_reference\EntityReferenceController + * @see \Drupal\system\Controller\EntityAutocompleteController */ - public function getMatches(FieldDefinitionInterface $field_definition, $entity_type, $bundle, $entity_id = '', $prefix = '', $string = '') { + public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') { $matches = array(); - $entity = NULL; - if ($entity_id !== 'NULL') { - $entity = $this->entityManager->getStorage($entity_type)->load($entity_id); - if (!$entity || !$entity->access('view')) { - throw new AccessDeniedHttpException(); - } - } - $handler = $this->selectionHandlerManager->getSelectionHandler($field_definition, $entity); + $options = array( + 'target_type' => $target_type, + 'handler' => $selection_handler, + 'handler_settings' => $selection_settings, + ); + $handler = $this->selectionManager->getInstance($options); if (isset($string)) { // Get an array of matching entities. - $widget = entity_get_form_display($entity_type, $bundle, 'default')->getComponent($field_definition->getName()); - $match_operator = !empty($widget['settings']['match_operator']) ? $widget['settings']['match_operator'] : 'CONTAINS'; + $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS'; $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10); // Loop through the entities and convert them into autocomplete output. @@ -101,7 +78,7 @@ public function getMatches(FieldDefinitionInterface $field_definition, $entity_t $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(String::decodeEntities(strip_tags($key))))); // Names containing commas or quotes must be wrapped in quotes. $key = Tags::encode($key); - $matches[] = array('value' => $prefix . $key, 'label' => $label); + $matches[] = array('value' => $key, 'label' => $label); } } } diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteTagsWidget.php similarity index 12% rename from core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php rename to core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteTagsWidget.php index f7b5ff3..2b2f003 100644 --- a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteTagsWidget.php @@ -2,85 +2,45 @@ /** * @file - * Contains \Drupal\entity_reference\Plugin\Field\FieldWidget\AutocompleteTagsWidget. + * Contains \Drupal\Core\Field\Plugin\Field\FieldWidget\AutocompleteTagsWidget. */ -namespace Drupal\entity_reference\Plugin\Field\FieldWidget; +namespace Drupal\Core\Field\Plugin\Field\FieldWidget; -use Drupal\Component\Utility\Tags; -use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; /** - * Plugin implementation of the 'entity_reference autocomplete-tags' widget. + * Plugin implementation of the 'entity_reference_autocomplete_tags' widget. * * @FieldWidget( * id = "entity_reference_autocomplete_tags", * label = @Translation("Autocomplete (Tags style)"), - * description = @Translation("An autocomplete text field."), + * description = @Translation("An autocomplete text field with tagging support."), * field_types = { * "entity_reference" * }, * multiple_values = TRUE * ) */ -class AutocompleteTagsWidget extends AutocompleteWidgetBase { +class EntityReferenceAutocompleteTagsWidget extends EntityReferenceAutocompleteWidget { /** * {@inheritdoc} */ - public static function defaultSettings() { - return array( - 'match_operator' => 'CONTAINS', - 'size' => '60', - 'autocomplete_type' => 'tags', - 'placeholder' => '', - ) + parent::defaultSettings(); + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + $element['target_id']['#tags'] = TRUE; + + return $element; } /** * {@inheritdoc} */ - public function elementValidate($element, FormStateInterface $form_state, $form) { - $value = array(); - // If a value was entered into the autocomplete. - $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getSelectionHandler($this->fieldDefinition); - $bundles = entity_get_bundles($this->getFieldSetting('target_type')); - $auto_create = $this->getSelectionHandlerSetting('auto_create'); - - if (!empty($element['#value'])) { - $value = array(); - foreach (Tags::explode($element['#value']) as $input) { - $match = FALSE; - - // Take "label (entity id)', match the ID from parenthesis when it's a - // number. - if (preg_match("/.+\((\d+)\)/", $input, $matches)) { - $match = $matches[1]; - } - // Match the ID when it's a string (e.g. for config entity types). - elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) { - $match = $matches[1]; - } - else { - // Try to get a match from the input string when the user didn't use - // the autocomplete but filled in a value manually. - $match = $handler->validateAutocompleteInput($input, $element, $form_state, $form, !$auto_create); - } - - if ($match) { - $value[] = array('target_id' => $match); - } - elseif ($auto_create && (count($this->getSelectionHandlerSetting('target_bundles')) == 1 || count($bundles) == 1)) { - // Auto-create item. See - // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). - $value[] = array('entity' => $this->createNewEntity($input, $element['#autocreate_uid'])); - } - } - }; - // Change the element['#parents'], so in setValueForElement() we populate - // the correct key. - array_pop($element['#parents']); - $form_state->setValueForElement($element, $value); + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + return $values['target_id']; } + } diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidgetBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php similarity index 60% rename from core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidgetBase.php rename to core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php index 40d0aaf..602eaf4 100644 --- a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidgetBase.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php @@ -2,10 +2,10 @@ /** * @file - * Contains \Drupal\entity_reference\Plugin\Field\FieldWidget\AutocompleteWidgetBase. + * Contains \Drupal\Core\Field\Plugin\Field\FieldWidget\AutocompleteWidget. */ -namespace Drupal\entity_reference\Plugin\Field\FieldWidget; +namespace Drupal\Core\Field\Plugin\Field\FieldWidget; use Drupal\Component\Utility\Tags; use Drupal\Core\Field\EntityReferenceFieldItemListInterface; @@ -16,9 +16,29 @@ use Symfony\Component\Validator\ConstraintViolationInterface; /** - * Parent plugin for entity reference autocomplete widgets. + * Plugin implementation of the 'entity_reference_autocomplete' widget. + * + * @FieldWidget( + * id = "entity_reference_autocomplete", + * label = @Translation("Autocomplete"), + * description = @Translation("An autocomplete text field."), + * field_types = { + * "entity_reference" + * } + * ) */ -abstract class AutocompleteWidgetBase extends WidgetBase { +class EntityReferenceAutocompleteWidget extends WidgetBase { + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return array( + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ) + parent::defaultSettings(); + } /** * {@inheritdoc} @@ -75,30 +95,27 @@ public function settingsSummary() { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { $entity = $items->getEntity(); - // Prepare the autocomplete route parameters. - $autocomplete_route_parameters = array( - 'type' => $this->getSetting('autocomplete_type'), - 'field_name' => $this->fieldDefinition->getName(), - 'entity_type' => $entity->getEntityTypeId(), - 'bundle_name' => $entity->bundle(), - ); - - if ($entity_id = $entity->id()) { - $autocomplete_route_parameters['entity_id'] = $entity_id; - } - $element += array( - '#type' => 'textfield', + '#type' => 'entity_autocomplete', + '#target_type' => $this->getFieldSetting('target_type'), + '#selection_handler' => $this->getFieldSetting('handler'), + '#selection_settings' => $this->getFieldSetting('handler_settings'), + // Entity reference field items are handling validation themselves via + // the 'ValidReference' constraint. + '#validate_reference' => FALSE, '#maxlength' => 1024, '#default_value' => implode(', ', $this->getLabels($items, $delta)), - '#autocomplete_route_name' => 'entity_reference.autocomplete', - '#autocomplete_route_parameters' => $autocomplete_route_parameters, '#size' => $this->getSetting('size'), '#placeholder' => $this->getSetting('placeholder'), - '#element_validate' => array(array($this, 'elementValidate')), - '#autocreate_uid' => ($entity instanceof EntityOwnerInterface) ? $entity->getOwnerId() : \Drupal::currentUser()->id(), ); + if ($this->getSelectionHandlerSetting('auto_create')) { + $element['#autocreate'] = array( + 'bundle' => $this->getAutocreateBundle(), + 'uid' => ($entity instanceof EntityOwnerInterface) ? $entity->getOwnerId() : \Drupal::currentUser()->id() + ); + } + return array('target_id' => $element); } @@ -110,9 +127,20 @@ public function errorElement(array $element, ConstraintViolationInterface $error } /** - * Validates an element. + * {@inheritdoc} */ - public function elementValidate($element, FormStateInterface $form_state, $form) { } + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + foreach ($values as $key => $value) { + // The entity_autocomplete form element returns an array when an entity + // was "autocreated", so we need to move it up a level. + if (is_array($value['target_id'])) { + unset($values[$key]['target_id']); + $values[$key] += $value['target_id']; + } + } + + return $values; + } /** * Gets the entity labels. @@ -144,43 +172,29 @@ protected function getLabels(EntityReferenceFieldItemListInterface $items, $delt } /** - * Creates a new entity from a label entered in the autocomplete input. - * - * @param string $label - * The entity label. - * @param int $uid - * The entity uid. + * Returns the name of the bundle which will be used for autocreated entities. * - * @return \Drupal\Core\Entity\EntityInterface + * @return string + * The bundle name. */ - protected function createNewEntity($label, $uid) { - $entity_manager = \Drupal::entityManager(); - $target_type = $this->getFieldSetting('target_type'); - $target_bundles = $this->getSelectionHandlerSetting('target_bundles'); - - // Get the bundle. - if (!empty($target_bundles)) { - $bundle = reset($target_bundles); - } - else { - $bundles = entity_get_bundles($target_type); - $bundle = reset($bundles); - } - - $entity_type = $entity_manager->getDefinition($target_type); - $bundle_key = $entity_type->getKey('bundle'); - $label_key = $entity_type->getKey('label'); - - $entity = $entity_manager->getStorage($target_type)->create(array( - $label_key => $label, - $bundle_key => $bundle, - )); - - if ($entity instanceof EntityOwnerInterface) { - $entity->setOwnerId($uid); + protected function getAutocreateBundle() { + $bundle = NULL; + if ($this->getSelectionHandlerSetting('auto_create')) { + // If the 'target_bundles' setting is restricted to a single choice, we + // can use that. + if (($target_bundles = $this->getSelectionHandlerSetting('target_bundles')) && count($target_bundles) == 1) { + $bundle = reset($target_bundles); + } + // Otherwise use the first bundle as a fallback. + else { + // @todo Expose a proper UI for choosing the bundle for autocreated + // entities in https://www.drupal.org/node/2412569. + $bundles = entity_get_bundles($this->getFieldSetting('target_type')); + $bundle = key($bundles); + } } - return $entity; + return $bundle; } /** @@ -197,15 +211,4 @@ protected function getSelectionHandlerSetting($setting_name) { return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL; } - /** - * Checks whether a content entity is referenced. - * - * @return bool - */ - protected function isContentReferenced() { - $target_type = $this->getFieldSetting('target_type'); - $target_type_info = \Drupal::entityManager()->getDefinition($target_type); - return $target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface'); - } - } diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 859cf07..26cd5d1 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -328,6 +328,12 @@ protected static function fromEntityUri(array $uri_parts, array $options, $uri) * A new Url object for a 'user-path:' URI. */ protected static function fromUserPathUri(array $uri_parts, array $options) { + // The PathValidator expects '' to be used for the frontpage, yet in + // 'user-path:' URIs, this is represented by the URI "user-path:" — i.e. by + // the empty string as the path component of the URI. + if ($uri_parts['path'] === '') { + $uri_parts['path'] = ''; + } $url = \Drupal::pathValidator() ->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options); // Allow specifying additional options. diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js index 70ccf0b..d9f230d 100644 --- a/core/misc/autocomplete.js +++ b/core/misc/autocomplete.js @@ -59,9 +59,15 @@ * @return {Boolean} */ function searchHandler(event) { - // Only search when the term is two characters or larger. + var options = autocomplete.options; var term = autocomplete.extractLastTerm(event.target.value); - return term.length >= autocomplete.minLength; + // Abort search if the first character is in firstCharacterBlacklist. + debugger; + if (term.length > 0 && options.firstCharacterBlacklist.indexOf(term[0]) !== -1) { + return false; + } + // Only search when the term is at least the minimum length. + return term.length >= options.minLength; } /** @@ -174,6 +180,11 @@ // Act on textfields with the "form-autocomplete" class. var $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete'); if ($autocomplete.length) { + // Allow options to be overriden per instance. + var blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist'); + var options = $.extend(autocomplete.options, { + firstCharacterBlacklist: (blacklist) ? blacklist : '' + }); // Use jQuery UI Autocomplete on the textfield. $autocomplete.autocomplete(autocomplete.options) .data("ui-autocomplete") @@ -194,8 +205,7 @@ */ autocomplete = { cache: {}, - // Exposes methods to allow overriding by contrib. - minLength: 1, + // Exposes options to allow overriding by contrib. splitValues: autocompleteSplitValues, extractLastTerm: extractLastTerm, // jQuery UI autocomplete options. @@ -204,7 +214,10 @@ focus: focusHandler, search: searchHandler, select: selectHandler, - renderItem: renderItem + renderItem: renderItem, + minLength: 1, + // Custom options, used by Drupal.autocomplete. + firstCharacterBlacklist: '' }, ajax: { dataType: 'json' diff --git a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml index ac9c4ee..0b7ccf6 100644 --- a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml +++ b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml @@ -25,7 +25,6 @@ content: settings: match_operator: CONTAINS size: 60 - autocomplete_type: tags placeholder: '' third_party_settings: { } created: diff --git a/core/modules/entity_reference/config/schema/entity_reference.schema.yml b/core/modules/entity_reference/config/schema/entity_reference.schema.yml index 1ee04d1..ec291fa 100644 --- a/core/modules/entity_reference/config/schema/entity_reference.schema.yml +++ b/core/modules/entity_reference/config/schema/entity_reference.schema.yml @@ -36,37 +36,3 @@ entity_reference.default.handler_settings: auto_create: type: boolean label: 'Create referenced entities if they don''t already exist' - -field.widget.settings.entity_reference_autocomplete_tags: - type: mapping - label: 'Entity reference autocomplete (Tags style) display format settings' - mapping: - match_operator: - type: string - label: 'Autocomplete matching' - size: - type: integer - label: 'Size of textfield' - autocomplete_type: - type: string - label: 'Autocomplete type' - placeholder: - type: label - label: 'Placeholder' - -field.widget.settings.entity_reference_autocomplete: - type: mapping - label: 'Entity reference autocomplete display format settings' - mapping: - match_operator: - type: string - label: 'Autocomplete matching' - size: - type: integer - label: 'Size of textfield' - autocomplete_type: - type: string - label: 'Autocomplete type' - placeholder: - type: label - label: 'Placeholder' diff --git a/core/modules/entity_reference/entity_reference.routing.yml b/core/modules/entity_reference/entity_reference.routing.yml deleted file mode 100644 index b2f73ff..0000000 --- a/core/modules/entity_reference/entity_reference.routing.yml +++ /dev/null @@ -1,7 +0,0 @@ -entity_reference.autocomplete: - path: '/entity_reference/autocomplete/{type}/{field_name}/{entity_type}/{bundle_name}/{entity_id}' - defaults: - _controller: '\Drupal\entity_reference\EntityReferenceController::handleAutocomplete' - entity_id: 'NULL' - requirements: - _access: 'TRUE' diff --git a/core/modules/entity_reference/entity_reference.services.yml b/core/modules/entity_reference/entity_reference.services.yml deleted file mode 100644 index 680cdf2..0000000 --- a/core/modules/entity_reference/entity_reference.services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - entity_reference.autocomplete: - class: Drupal\entity_reference\EntityReferenceAutocomplete - arguments: ['@entity.manager', '@plugin.manager.entity_reference_selection'] diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidget.php b/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidget.php deleted file mode 100644 index 1e33d80..0000000 --- a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteWidget.php +++ /dev/null @@ -1,86 +0,0 @@ - 'CONTAINS', - 'size' => '60', - 'autocomplete_type' => 'tags', - 'placeholder' => '', - ) + parent::defaultSettings(); - } - - /** - * {@inheritdoc} - */ - public function elementValidate($element, FormStateInterface $form_state, $form) { - $auto_create = $this->getSelectionHandlerSetting('auto_create'); - - // If a value was entered into the autocomplete. - $value = NULL; - if (!empty($element['#value'])) { - // Take "label (entity id)', match the id from parenthesis. - // @todo: Lookup the entity type's ID data type and use it here. - // https://drupal.org/node/2107249 - if ($this->isContentReferenced() && preg_match("/.+\((\d+)\)/", $element['#value'], $matches)) { - $value = $matches[1]; - } - elseif (preg_match("/.+\(([\w.]+)\)/", $element['#value'], $matches)) { - $value = $matches[1]; - } - if ($value === NULL) { - // Try to get a match from the input string when the user didn't use the - // autocomplete but filled in a value manually. - $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getSelectionHandler($this->fieldDefinition); - $value = $handler->validateAutocompleteInput($element['#value'], $element, $form_state, $form, !$auto_create); - } - - if (!$value && $auto_create && (count($this->getSelectionHandlerSetting('target_bundles')) == 1)) { - // Auto-create item. See - // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). - $value = array( - 'entity' => $this->createNewEntity($element['#value'], $element['#autocreate_uid']), - // Keep the weight property. - '_weight' => $element['#weight'], - ); - // Change the element['#parents'], so in setValueForElement() we - // populate the correct key. - array_pop($element['#parents']); - } - } - $form_state->setValueForElement($element, $value); - } -} diff --git a/core/modules/field_ui/src/Tests/EntityDisplayTest.php b/core/modules/field_ui/src/Tests/EntityDisplayTest.php index 09887da..7753d41 100644 --- a/core/modules/field_ui/src/Tests/EntityDisplayTest.php +++ b/core/modules/field_ui/src/Tests/EntityDisplayTest.php @@ -17,7 +17,7 @@ */ class EntityDisplayTest extends KernelTestBase { - public static $modules = array('field_ui', 'field', 'entity_test', 'user', 'text', 'field_test', 'node', 'system', 'entity_reference'); + public static $modules = array('field_ui', 'field', 'entity_test', 'user', 'text', 'field_test', 'node', 'system'); protected function setUp() { parent::setUp(); @@ -307,7 +307,7 @@ public function testRenameDeleteBundle() { $dependencies = $new_form_display->calculateDependencies(); $expected_form_dependencies = array( 'config' => array('field.field.node.article_rename.body', 'node.type.article_rename'), - 'module' => array('entity_reference', 'text') + 'module' => array('text') ); $this->assertEqual($expected_form_dependencies, $dependencies); diff --git a/core/modules/forum/config/install/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/install/core.entity_form_display.node.forum.default.yml index cc84152..8731335 100644 --- a/core/modules/forum/config/install/core.entity_form_display.node.forum.default.yml +++ b/core/modules/forum/config/install/core.entity_form_display.node.forum.default.yml @@ -28,7 +28,6 @@ content: settings: match_operator: CONTAINS size: 60 - autocomplete_type: tags placeholder: '' third_party_settings: { } created: diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index e92fe53..24a6237 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -9,11 +9,13 @@ use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Entity\Element\EntityAutocomplete; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\link\LinkItemInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -44,20 +46,49 @@ public static function defaultSettings() { /** * Gets the URI without the 'user-path:' scheme, for display while editing. * + * This method is the inverse of ::getUserEnteredStringAsUri(). + * * @param string $uri * The URI to get the displayable string for. * * @return string + * + * @see getUserEnteredStringAsUri() */ protected static function getUriAsDisplayableString($uri) { $scheme = parse_url($uri, PHP_URL_SCHEME); + + // By default, the displayable string is the URI. + $displayable_string = $uri; + + // A different displayable string may be chosen in case of the 'user-path:' + // or 'entity:' built-in schemes. if ($scheme === 'user-path') { $uri_reference = explode(':', $uri, 2)[1]; + // Prepend '/', because that's what we let users enter. + // @todo This does not yet support the route plus a fragment or + // querystring. That is blocked on https://www.drupal.org/node/2417647, + // which will already store things in exactly the same way as we expect + // the user to enter them. i.e. this translation layer will then become + // obsolete. + $displayable_string = '/' . $uri_reference; } - else { - $uri_reference = $uri; + elseif ($scheme === 'entity') { + list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); + // Show the 'entity:' URI as the entity autocomplete would, but only if: + // - the entity could be loaded, and; + // - the current user is allowed to view the entity (otherwise we have a + // information disclosure security problem). + $entity_manager = \Drupal::entityManager(); + if ($entity_manager->getDefinition($entity_type, FALSE)) { + $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id); + if ($entity && $entity->access('view')) { + $displayable_string = $entity->label() . ' (' . $entity_id . ')'; + } + } } - return $uri_reference; + + return $displayable_string;; } /** @@ -65,18 +96,30 @@ protected static function getUriAsDisplayableString($uri) { * * Schemeless URIs are treated as 'user-path:' URIs. * + * This method is the inverse of ::getUriAsDisplayableString(). + * * @param string $string * The user-entered string. * * @return string - * The URI, if a non-empty $string was passed. + * The URI, if a non-empty $uri was passed. + * + * @see getUriAsDisplayableString() */ protected static function getUserEnteredStringAsUri($string) { + $entity_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($string); + if ($entity_id !== NULL) { + $string = 'entity:node/' . $entity_id; + } + if (!empty($string)) { // Users can enter relative URLs, but we need a valid URI, so add an // explicit scheme when necessary. if (parse_url($string, PHP_URL_SCHEME) === NULL) { - return 'user-path:' . $string; + // @todo The ltrim() call here will become obsolete as soon as + // https://www.drupal.org/node/241764 lands; see earlier todo for + // more info. + $string = 'user-path:' . ltrim($string, '/'); } } return $string; @@ -87,6 +130,13 @@ protected static function getUserEnteredStringAsUri($string) { */ public static function validateUriElement($element, FormStateInterface $form_state, $form) { $uri = static::getUserEnteredStringAsUri($element['#value']); + $form_state->setValueForElement($element, $uri); + + // If getUserEnteredStringAsUri() mapped the entered value is mapped to a + // 'user-path:' URI , ensure the raw value begins with '/', '?' or '#'. + if (parse_url($uri, PHP_URL_SCHEME) === 'user-path' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE)) { + $form_state->setError($element, t('Manually entered paths should start with /, ? or #.')); + } // If the URI is empty or not well-formed, the link field type's validation // constraint will detect it. @@ -101,6 +151,15 @@ public static function validateUriElement($element, FormStateInterface $form_sta $disallowed = $disallowed || (!\Drupal::currentUser()->hasPermission('link to any page') && !$url->access()); // Disallow external URLs using untrusted protocols. $disallowed = $disallowed || ($url->isExternal() && !in_array(parse_url($uri, PHP_URL_SCHEME), UrlHelper::getAllowedProtocols())); + // Disallow routed URLs that don't exist. + if (!$disallowed && $url->isRouted()) { + try { + $url->toString(); + } + catch (RouteNotFoundException $e) { + $disallowed = TRUE; + } + } if ($disallowed) { $form_state->setError($element, t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => static::getUriAsDisplayableString($uri)])); @@ -131,18 +190,22 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // If the field is configured to support internal links, it cannot use the // 'url' form element and we have to do the validation ourselves. if ($this->supportsInternalLinks()) { - $element['uri']['#type'] = 'textfield'; + $element['uri']['#type'] = 'entity_autocomplete'; + // @todo This should be a setting? + $element['uri']['#target_type'] = 'node'; + // Disable autocompletion when the first character is '/', '#' or '?'. + $element['uri']['#attributes']['data-autocomplete-first-character-blacklist'] = '/#?'; } // If the field is configured to allow only internal links, add a useful // element prefix. if (!$this->supportsExternalLinks()) { - $element['uri']['#field_prefix'] = \Drupal::url('', array(), array('absolute' => TRUE)); + $element['uri']['#field_prefix'] = rtrim(\Drupal::url('', array(), array('absolute' => TRUE)), '/'); } // If the field is configured to allow both internal and external links, // show a useful description. elseif ($this->supportsExternalLinks() && $this->supportsInternalLinks()) { - $element['uri']['#description'] = $this->t('This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')); + $element['uri']['#description'] = $this->t('This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => '/node/add', '%drupal' => 'http://drupal.org')); } $element['title'] = array( diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index 05a427e..8cce8f3 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -25,7 +25,7 @@ class LinkFieldTest extends WebTestBase { * * @var array */ - public static $modules = ['entity_test', 'link']; + public static $modules = ['entity_test', 'link', 'node']; /** * A field to use in this test class. @@ -93,27 +93,45 @@ function testURLValidation() { // Create a path alias. \Drupal::service('path.alias_storage')->save('admin', 'a/path/alias'); - // Define some valid URLs. + + // Create a node to test the link widget. + $node = $this->drupalCreateNode(); + + // Define some valid URLs (keys are the entered values, values are the + // strings displayed to the user). $valid_external_entries = array( - 'http://www.example.com/', + 'http://www.example.com/' => 'http://www.example.com/', ); $valid_internal_entries = array( - 'entity_test/add', - 'a/path/alias', - 'entity:user/1', + '/entity_test/add' => '/entity_test/add', + '/a/path/alias' => '/a/path/alias', + '#example' => '#example', + '?example=llama' => '?example=llama', + // Entity reference autocomplete value. + $node->label() . ' (1)' => $node->label() . ' (1)', + # URI for an entity that exists. + 'entity:user/1' => 'entity:user/1', + # URI for an entity that doesn't exist, but with a valid ID. + 'entity:user/999999' => 'entity:user/999999', + # URI for an entity that doesn't exist, with an invalid ID. + 'entity:user/invalid-parameter' => 'entity:user/invalid-parameter', ); // Define some invalid URLs. + $validation_error_1 = "The path '@link_path' is either invalid or you do not have access to it."; + $validation_error_2 = 'Manually entered paths should start with /, ? or #.'; $invalid_external_entries = array( // Missing protcol - 'not-an-url', + 'not-an-url' => $validation_error_2, // Invalid protocol - 'invalid://not-a-valid-protocol', + 'invalid://not-a-valid-protocol' => $validation_error_1, // Missing host name - 'http://', + 'http://' => $validation_error_1, ); $invalid_internal_entries = array( - 'non/existing/path', + '/non/existing/path' => $validation_error_1, + 'no-leading-slash' => $validation_error_2, + 'entity:non_existing_entity_type/yar' => $validation_error_1, ); // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. @@ -142,15 +160,15 @@ function testURLValidation() { * An array of valid URL entries. */ protected function assertValidEntries($field_name, array $valid_entries) { - foreach ($valid_entries as $value) { + foreach ($valid_entries as $uri => $string) { $edit = array( - "{$field_name}[0][uri]" => $value, + "{$field_name}[0][uri]" => $uri, ); $this->drupalPostForm('entity_test/add', $edit, t('Save')); preg_match('|entity_test/manage/(\d+)|', $this->url, $match); $id = $match[1]; $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); - $this->assertRaw($value); + $this->assertRaw($string); } } @@ -163,12 +181,12 @@ protected function assertValidEntries($field_name, array $valid_entries) { * An array of invalid URL entries. */ protected function assertInvalidEntries($field_name, array $invalid_entries) { - foreach ($invalid_entries as $invalid_value) { + foreach ($invalid_entries as $invalid_value => $error_message) { $edit = array( "{$field_name}[0][uri]" => $invalid_value, ); $this->drupalPostForm('entity_test/add', $edit, t('Save')); - $this->assertText(t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $invalid_value))); + $this->assertText(t($error_message, array('@link_path' => $invalid_value))); } } diff --git a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php index 67411f2..cfd0e3e 100644 --- a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php +++ b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php @@ -70,7 +70,7 @@ function testMenuLanguage() { $this->assertOptionSelected('edit-langcode', $edit['langcode'], 'The menu language was correctly selected.'); // Test menu link language. - $link_path = ''; + $link_path = '/'; // Add a menu link. $link_title = $this->randomString(); diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php index 8763132..ccdabdf 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -105,7 +105,7 @@ function testMenu() { $this->verifyAccess(403); foreach ($this->items as $item) { - // Paths were set as 'node/$nid'. + // Menu link URIs are of the form "user-path:node/$nid". $node = Node::load(str_replace('user-path:node/', '', $item->link->uri)); $this->verifyMenuLink($item, $node); } @@ -266,7 +266,7 @@ function doMenuTests() { $this->clickLink(t('Add link')); $link_title = $this->randomString(); - $this->drupalPostForm(NULL, array('link[0][uri]' => '', 'title[0][value]' => $link_title), t('Save')); + $this->drupalPostForm(NULL, array('link[0][uri]' => '/', 'title[0][value]' => $link_title), t('Save')); $this->assertUrl(Url::fromRoute('entity.menu.edit_form', ['menu' => $menu_name])); // Test the 'Edit' operation. $this->clickLink(t('Edit')); @@ -301,9 +301,9 @@ function doMenuTests() { $this->doMenuLinkFormDefaultsTest(); // Add menu links. - $item1 = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE); - $item2 = $this->addMenuLink($item1->getPluginId(), 'node/' . $node2->id(), $menu_name, FALSE); - $item3 = $this->addMenuLink($item2->getPluginId(), 'node/' . $node3->id(), $menu_name); + $item1 = $this->addMenuLink('', '/node/' . $node1->id(), $menu_name, TRUE); + $item2 = $this->addMenuLink($item1->getPluginId(), '/node/' . $node2->id(), $menu_name, FALSE); + $item3 = $this->addMenuLink($item2->getPluginId(), '/node/' . $node3->id(), $menu_name); // Hierarchy // <$menu_name> @@ -337,10 +337,10 @@ function doMenuTests() { $this->verifyMenuLink($item3, $node3, $item2, $node2); // Add more menu links. - $item4 = $this->addMenuLink('', 'node/' . $node4->id(), $menu_name); - $item5 = $this->addMenuLink($item4->getPluginId(), 'node/' . $node5->id(), $menu_name); + $item4 = $this->addMenuLink('', '/node/' . $node4->id(), $menu_name); + $item5 = $this->addMenuLink($item4->getPluginId(), '/node/' . $node5->id(), $menu_name); // Create a menu link pointing to an alias. - $item6 = $this->addMenuLink($item4->getPluginId(), 'node5', $menu_name, TRUE, '0'); + $item6 = $this->addMenuLink($item4->getPluginId(), '/node5', $menu_name, TRUE, '0'); // Hierarchy // <$menu_name> @@ -427,7 +427,7 @@ function doMenuTests() { // item's weight doesn't get changed because of the old hardcoded delta=50. $items = array(); for ($i = -50; $i <= 51; $i++) { - $items[$i] = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE, strval($i)); + $items[$i] = $this->addMenuLink('', '/node/' . $node1->id(), $menu_name, TRUE, strval($i)); } $this->assertMenuLink($items[51]->getPluginId(), array('weight' => '51')); @@ -454,7 +454,7 @@ function doMenuTests() { $this->assertMenuLink($item7->getPluginId(), array('url' => 'http://drupal.org')); // Add menu item. - $item8 = $this->addMenuLink('', '', $menu_name); + $item8 = $this->addMenuLink('', '/', $menu_name); $this->assertMenuLink($item8->getPluginId(), array('route_name' => '')); $this->drupalGet(''); $this->assertResponse(200); @@ -491,21 +491,20 @@ function testMenuQueryAndFragment() { $this->drupalLogin($this->admin_user); // Make a path with query and fragment on. - $path = 'test-page?arg1=value1&arg2=value2'; + $path = '/test-page?arg1=value1&arg2=value2'; $item = $this->addMenuLink('', $path); $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); $this->assertFieldByName('link[0][uri]', $path, 'Path is found with both query and fragment.'); // Now change the path to something without query and fragment. - $path = 'test-page'; + $path = '/test-page'; $this->drupalPostForm('admin/structure/menu/item/' . $item->id() . '/edit', array('link[0][uri]' => $path), t('Save')); $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); $this->assertFieldByName('link[0][uri]', $path, 'Path no longer has query or fragment.'); - // Use #fragment and ensure that saving it does not loose its - // content. - $path = '?arg1=value#fragment'; + // Use /#fragment and ensure that saving it does not loose its content. + $path = '/?arg1=value#fragment'; $item = $this->addMenuLink('', $path); $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); @@ -544,7 +543,7 @@ function testUnpublishedNodeMenuItem() { 'status' => NODE_NOT_PUBLISHED, )); - $item = $this->addMenuLink('', 'node/' . $node->id()); + $item = $this->addMenuLink('', '/node/' . $node->id()); $this->modifyMenuLink($item); // Test that a user with 'administer menu' but without 'bypass node access' @@ -561,7 +560,7 @@ function testUnpublishedNodeMenuItem() { public function testBlockContextualLinks() { $this->drupalLogin($this->drupalCreateUser(array('administer menu', 'access contextual links', 'administer blocks'))); $custom_menu = $this->addCustomMenu(); - $this->addMenuLink('', '', $custom_menu->id()); + $this->addMenuLink('', '/', $custom_menu->id()); $block = $this->drupalPlaceBlock('system_menu_block:' . $custom_menu->id(), array('label' => 'Custom menu', 'provider' => 'system')); $this->drupalGet('test-page'); @@ -603,7 +602,7 @@ public function testBlockContextualLinks() { * @return \Drupal\menu_link_content\Entity\MenuLinkContent * A menu link entity. */ - function addMenuLink($parent = '', $path = '', $menu_name = 'tools', $expanded = FALSE, $weight = '0') { + function addMenuLink($parent = '', $path = '/', $menu_name = 'tools', $expanded = FALSE, $weight = '0') { // View add menu link page. $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); $this->assertResponse(200); @@ -637,7 +636,7 @@ function addMenuLink($parent = '', $path = '', $menu_name = 'tools', $exp * Attempts to add menu link with invalid path or no access permission. */ function addInvalidMenuLink() { - foreach (array('-&-', 'admin/people/permissions', '#') as $link_path) { + foreach (array('/-&-', '/admin/people/permissions') as $link_path) { $edit = array( 'link[0][uri]' => $link_path, 'title[0][value]' => 'title', diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 40d0053..bf6ff6a 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -400,7 +400,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'settings' => array( 'match_operator' => 'CONTAINS', 'size' => '60', - 'autocomplete_type' => 'tags', 'placeholder' => '', ), )) diff --git a/core/modules/node/src/Tests/NodeCreationTest.php b/core/modules/node/src/Tests/NodeCreationTest.php index 7ec85eb..3caf0bd 100644 --- a/core/modules/node/src/Tests/NodeCreationTest.php +++ b/core/modules/node/src/Tests/NodeCreationTest.php @@ -158,7 +158,7 @@ public function testAuthorAutocomplete() { $this->drupalGet('node/add/page'); - $result = $this->xpath('//input[@id="edit-uid-0-target-id" and contains(@data-autocomplete-path, "/entity_reference/autocomplete/tags/uid/node/page")]'); + $result = $this->xpath('//input[@id="edit-uid-0-target-id" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/user/default")]'); $this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion'); } diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml index c3540f9..6d0f3e8 100644 --- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml +++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml @@ -25,7 +25,6 @@ content: settings: match_operator: CONTAINS size: 60 - autocomplete_type: tags placeholder: '' third_party_settings: { } created: diff --git a/core/modules/shortcut/src/Tests/ShortcutLinksTest.php b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php index 40c230d..96d8aaf 100644 --- a/core/modules/shortcut/src/Tests/ShortcutLinksTest.php +++ b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php @@ -41,13 +41,13 @@ public function testShortcutLinkAdd() { // Create some paths to test. $test_cases = [ - '', - 'admin', - 'admin/config/system/site-information', - 'node/' . $this->node->id() . '/edit', - $path['alias'], - 'router_test/test2', - 'router_test/test3/value', + '/', + '/admin', + '/admin/config/system/site-information', + '/node/' . $this->node->id() . '/edit', + '/' . $path['alias'], + '/router_test/test2', + '/router_test/test3/value', ]; // Check that each new shortcut links where it should. @@ -61,7 +61,7 @@ public function testShortcutLinkAdd() { $this->assertResponse(200); $saved_set = ShortcutSet::load($set->id()); $paths = $this->getShortcutInformation($saved_set, 'link'); - $this->assertTrue(in_array('user-path:' . $test_path, $paths), 'Shortcut created: ' . $test_path); + $this->assertTrue(in_array('user-path:' . ltrim('/', $test_path), $paths), 'Shortcut created: ' . $test_path); $this->assertLink($title, 0, String::format('Shortcut link %url found on the page.', ['%url' => $test_path])); } $saved_set = ShortcutSet::load($set->id()); @@ -81,15 +81,15 @@ public function testShortcutLinkAdd() { $title = $this->randomMachineName(); $form_data = [ 'title[0][value]' => $title, - 'link[0][uri]' => 'admin', + 'link[0][uri]' => '/admin', ]; $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); $this->assertResponse(200); - $this->assertRaw(t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => 'admin'])); + $this->assertRaw(t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => '/admin'])); $form_data = [ 'title[0][value]' => $title, - 'link[0][uri]' => 'node', + 'link[0][uri]' => '/node', ]; $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); $this->assertLink($title, 0, 'Shortcut link found on the page.'); @@ -136,7 +136,7 @@ public function testShortcutLinkRename() { $shortcuts = $set->getShortcuts(); $shortcut = reset($shortcuts); - $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name, 'link[0][uri]' => $shortcut->link->uri), t('Save')); + $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $new_link_name), t('Save')); $saved_set = ShortcutSet::load($set->id()); $titles = $this->getShortcutInformation($saved_set, 'title'); $this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name); @@ -150,14 +150,14 @@ public function testShortcutLinkChangePath() { $set = $this->set; // Tests changing a shortcut path. - $new_link_path = 'admin/config'; + $new_link_path = '/admin/config'; $shortcuts = $set->getShortcuts(); $shortcut = reset($shortcuts); $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $shortcut->getTitle(), 'link[0][uri]' => $new_link_path), t('Save')); $saved_set = ShortcutSet::load($set->id()); $paths = $this->getShortcutInformation($saved_set, 'link'); - $this->assertTrue(in_array('user-path:' . $new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path); + $this->assertTrue(in_array('user-path:' . ltrim($new_link_path, '/'), $paths), 'Shortcut path changed: ' . $new_link_path); $this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.'); } diff --git a/core/modules/entity_reference/src/EntityReferenceController.php b/core/modules/system/src/Controller/EntityAutocompleteController.php similarity index 20% rename from core/modules/entity_reference/src/EntityReferenceController.php rename to core/modules/system/src/Controller/EntityAutocompleteController.php index afc4cc0..3da9b85 100644 --- a/core/modules/entity_reference/src/EntityReferenceController.php +++ b/core/modules/system/src/Controller/EntityAutocompleteController.php @@ -2,39 +2,39 @@ /** * @file - * Contains \Drupal\entity_reference/EntityReferenceController. + * Contains \Drupal\system\Controller\EntityAutocompleteController. */ -namespace Drupal\entity_reference; +namespace Drupal\system\Controller; use Drupal\Component\Utility\Tags; use Drupal\Component\Utility\Unicode; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityAutocompleteMatcher; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** - * Defines route controller for entity reference. + * Defines a route controller for entity autocomplete form elements. */ -class EntityReferenceController extends ControllerBase { +class EntityAutocompleteController extends ControllerBase { /** - * The autocomplete helper for entity references. + * The autocomplete matcher for entity references. * - * @var \Drupal\entity_reference\EntityReferenceAutocomplete + * @var \Drupal\Core\Entity\EntityAutocompleteMatcher */ - protected $entityReferenceAutocomplete; + protected $matcher; /** - * Constructs a EntityReferenceController object. + * Constructs a EntityAutocompleteController object. * - * @param \Drupal\entity_reference\EntityReferenceAutocomplete $entity_reference_autocompletion - * The autocompletion helper for entity references. + * @param \Drupal\Core\Entity\EntityAutocompleteMatcher $matcher + * The autocomplete matcher for entity references. */ - public function __construct(EntityReferenceAutocomplete $entity_reference_autocompletion) { - $this->entityReferenceAutocomplete = $entity_reference_autocompletion; + public function __construct(EntityAutocompleteMatcher $matcher) { + $this->matcher = $matcher; } /** @@ -42,61 +42,39 @@ public function __construct(EntityReferenceAutocomplete $entity_reference_autoco */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity_reference.autocomplete') + $container->get('entity.autocomplete_matcher') ); } /** * Autocomplete the label of an entity. * - * @param Request $request + * @param \Symfony\Component\HttpFoundation\Request $request * The request object that contains the typed tags. - * @param string $type - * The widget type (i.e. 'single' or 'tags'). - * @param string $field_name - * The name of the entity reference field. - * @param string $entity_type - * The entity type. - * @param string $bundle_name - * The bundle name. - * @param string $entity_id - * (optional) The entity ID the entity reference field is attached to. - * Defaults to ''. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * Throws access denied when either the field storage or field does not - * exists or the user does not have access to edit the field. + * @param string $target_type + * The ID of the target entity type. + * @param string $selection_handler + * The plugin ID of the entity reference selection handler. + * @param string $selection_settings + * The settings that will be passed to the selection handler. * * @return \Symfony\Component\HttpFoundation\JsonResponse - * The matched labels as json. + * The matched entity labels as a JSON response. */ - public function handleAutocomplete(Request $request, $type, $field_name, $entity_type, $bundle_name, $entity_id) { - $definitions = $this->entityManager()->getFieldDefinitions($entity_type, $bundle_name); + public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings = '') { + $matches = array(); + // Get the typed string from the URL, if it exists. + if ($input = $request->query->get('q')) { + $typed_string = Tags::explode($input); + $typed_string = Unicode::strtolower(array_pop($typed_string)); - if (!isset($definitions[$field_name])) { - throw new AccessDeniedHttpException(); - } + // Selection settings are passed in as an encoded serialized array. + $selection_settings = $selection_settings ? unserialize(base64_decode($selection_settings)) : array(); - $field_definition = $definitions[$field_name]; - $access_control_handler = $this->entityManager()->getAccessControlHandler($entity_type); - if ($field_definition->getType() != 'entity_reference' || !$access_control_handler->fieldAccess('edit', $field_definition)) { - throw new AccessDeniedHttpException(); + $matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string); } - // Get the typed string, if exists from the URL. - $items_typed = $request->query->get('q'); - $items_typed = Tags::explode($items_typed); - $last_item = Unicode::strtolower(array_pop($items_typed)); - - $prefix = ''; - // The user entered a comma-separated list of entity labels, so we generate - // a prefix. - if ($type == 'tags' && !empty($last_item)) { - $prefix = count($items_typed) ? Tags::implode($items_typed) . ', ' : ''; - } - - $matches = $this->entityReferenceAutocomplete->getMatches($field_definition, $entity_type, $bundle_name, $entity_id, $prefix, $last_item); - return new JsonResponse($matches); } + } diff --git a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php new file mode 100644 index 0000000..a2ce824 --- /dev/null +++ b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php @@ -0,0 +1,292 @@ +installSchema('system', array('router')); + \Drupal::service('router.builder')->rebuild(); + + $this->testUser = User::create(array( + 'name' => 'foobar1', + 'mail' => 'foobar1@example.com', + )); + $this->testUser->save(); + \Drupal::service('current_user')->setAccount($this->testUser); + + $this->testAutocreateUser = User::create(array( + 'name' => 'foobar2', + 'mail' => 'foobar2@example.com', + )); + $this->testAutocreateUser->save(); + + for ($i = 1; $i < 3; $i++) { + $entity = EntityTest::create(array( + 'name' => $this->randomMachineName() + )); + $entity->save(); + $this->referencedEntities[] = $entity; + } + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'test_entity_autocomplete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['single'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + ); + $form['single_autocreate'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#autocreate' => array( + 'bundle' => 'entity_test', + ), + ); + $form['single_autocreate_specific_uid'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#autocreate' => array( + 'bundle' => 'entity_test', + 'uid' => $this->testAutocreateUser->id(), + ), + ); + + $form['tags'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#tags' => TRUE, + ); + $form['tags_autocreate'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#tags' => TRUE, + '#autocreate' => array( + 'bundle' => 'entity_test', + ), + ); + $form['tags_autocreate_specific_uid'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#tags' => TRUE, + '#autocreate' => array( + 'bundle' => 'entity_test', + 'uid' => $this->testAutocreateUser->id(), + ), + ); + + $form['single_no_validate'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#validate_reference' => FALSE, + ); + $form['single_autocreate_no_validate'] = array( + '#type' => 'entity_autocomplete', + '#target_type' => 'entity_test', + '#autocreate' => array( + 'bundle' => 'entity_test', + ), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { } + + /** + * Tests valid entries in the EntityAutocomplete Form API element. + */ + public function testValidEntityAutocompleteElement() { + $form_state = (new FormState()) + ->setValues([ + 'single' => $this->getAutocompleteInput($this->referencedEntities[0]), + 'single_autocreate' => 'single - autocreated entity label', + 'single_autocreate_specific_uid' => 'single - autocreated entity label with specific uid', + 'tags' => $this->getAutocompleteInput($this->referencedEntities[0]) . ', ' . $this->getAutocompleteInput($this->referencedEntities[1]), + 'tags_autocreate' => + $this->getAutocompleteInput($this->referencedEntities[0]) + . ', tags - autocreated entity label, ' + . $this->getAutocompleteInput($this->referencedEntities[1]), + 'tags_autocreate_specific_uid' => + $this->getAutocompleteInput($this->referencedEntities[0]) + . ', tags - autocreated entity label with specific uid, ' + . $this->getAutocompleteInput($this->referencedEntities[1]), + ]); + $form_builder = $this->container->get('form_builder'); + $form_builder->submitForm($this, $form_state); + + // Valid form state. + $this->assertEqual(count($form_state->getErrors()), 0); + + // Test the 'single' element. + $this->assertEqual($form_state->getValue('single'), $this->referencedEntities[0]->id()); + + // Test the 'single_autocreate' element. + $value = $form_state->getValue('single_autocreate'); + $this->assertEqual($value['entity']->label(), 'single - autocreated entity label'); + $this->assertEqual($value['entity']->bundle(), 'entity_test'); + $this->assertEqual($value['entity']->getOwnerId(), $this->testUser->id()); + + // Test the 'single_autocreate_specific_uid' element. + $value = $form_state->getValue('single_autocreate_specific_uid'); + $this->assertEqual($value['entity']->label(), 'single - autocreated entity label with specific uid'); + $this->assertEqual($value['entity']->bundle(), 'entity_test'); + $this->assertEqual($value['entity']->getOwnerId(), $this->testAutocreateUser->id()); + + // Test the 'tags' element. + $expected = array( + array('target_id' => $this->referencedEntities[0]->id()), + array('target_id' => $this->referencedEntities[1]->id()), + ); + $this->assertEqual($form_state->getValue('tags'), $expected); + + // Test the 'single_autocreate' element. + $value = $form_state->getValue('tags_autocreate'); + // First value is an existing entity. + $this->assertEqual($value[0]['target_id'], $this->referencedEntities[0]->id()); + // Second value is an autocreated entity. + $this->assertTrue(!isset($value[1]['target_id'])); + $this->assertEqual($value[1]['entity']->label(), 'tags - autocreated entity label'); + $this->assertEqual($value[1]['entity']->getOwnerId(), $this->testUser->id()); + // Third value is an existing entity. + $this->assertEqual($value[2]['target_id'], $this->referencedEntities[1]->id()); + + // Test the 'tags_autocreate_specific_uid' element. + $value = $form_state->getValue('tags_autocreate_specific_uid'); + // First value is an existing entity. + $this->assertEqual($value[0]['target_id'], $this->referencedEntities[0]->id()); + // Second value is an autocreated entity. + $this->assertTrue(!isset($value[1]['target_id'])); + $this->assertEqual($value[1]['entity']->label(), 'tags - autocreated entity label with specific uid'); + $this->assertEqual($value[1]['entity']->getOwnerId(), $this->testAutocreateUser->id()); + // Third value is an existing entity. + $this->assertEqual($value[2]['target_id'], $this->referencedEntities[1]->id()); + } + + /** + * Tests invalid entries in the EntityAutocomplete Form API element. + */ + public function testInvalidEntityAutocompleteElement() { + $form_builder = $this->container->get('form_builder'); + + // Test 'single' with a entity label that doesn't exist + $form_state = (new FormState()) + ->setValues([ + 'single' => 'single - non-existent label', + ]); + $form_builder->submitForm($this, $form_state); + $this->assertEqual(count($form_state->getErrors()), 1); + $this->assertEqual($form_state->getErrors()['single'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label'))); + + // Test 'single' with a entity ID that doesn't exist. + $form_state = (new FormState()) + ->setValues([ + 'single' => 'single - non-existent label (42)', + ]); + $form_builder->submitForm($this, $form_state); + $this->assertEqual(count($form_state->getErrors()), 1); + $this->assertEqual($form_state->getErrors()['single'], t('The referenced entity (%type: %id) does not exist.', array('%type' => 'entity_test', '%id' => 42))); + + // Do the same tests as above but on an element with '#validate_reference' + // set to FALSE. + $form_state = (new FormState()) + ->setValues([ + 'single_no_validate' => 'single - non-existent label', + 'single_autocreate_no_validate' => 'single - autocreate non-existent label' + ]); + $form_builder->submitForm($this, $form_state); + + // The element without 'autocreate' support still has to emit a warning when + // the input doesn't end with an entity ID enclosed in parentheses. + $this->assertEqual(count($form_state->getErrors()), 1); + $this->assertEqual($form_state->getErrors()['single_no_validate'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label'))); + + $form_state = (new FormState()) + ->setValues([ + 'single_no_validate' => 'single - non-existent label (42)', + 'single_autocreate_no_validate' => 'single - autocreate non-existent label (43)' + ]); + $form_builder->submitForm($this, $form_state); + + // The input is complete (i.e. contains an entity ID at the ent), no errors + // are triggered. + $this->assertEqual(count($form_state->getErrors()), 0); + } + + + /** + * Returns an entity label in the format needed by the EntityAutocomplete + * element. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A Drupal entity. + * + * @return string + * A string that can be used as a value for EntityAutocomplete elements. + */ + protected function getAutocompleteInput(EntityInterface $entity) { + return $entity->label() . ' (' . $entity->id() . ')'; + } + +} diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceAutocompleteTest.php b/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php similarity index 50% rename from core/modules/entity_reference/src/Tests/EntityReferenceAutocompleteTest.php rename to core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php index ae9ac0c..f6fa256 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceAutocompleteTest.php +++ b/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php @@ -2,25 +2,23 @@ /** * @file - * Contains \Drupal\entity_reference\Tests\EntityReferenceAutocompleteTest. + * Contains \Drupal\system\Tests\Entity\EntityAutocompleteTest. */ -namespace Drupal\entity_reference\Tests; +namespace Drupal\system\Tests\Entity; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Tags; -use Drupal\entity_reference\EntityReferenceController; -use Drupal\system\Tests\Entity\EntityUnitTestBase; +use Drupal\system\Controller\EntityAutocompleteController; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Tests the autocomplete functionality. * - * @group entity_reference + * @group Entity */ -class EntityReferenceAutocompleteTest extends EntityUnitTestBase { +class EntityAutocompleteTest extends EntityUnitTestBase { /** * The entity type used in this test. @@ -37,52 +35,32 @@ class EntityReferenceAutocompleteTest extends EntityUnitTestBase { protected $bundle = 'entity_test'; /** - * The name of the field used in this test. - * - * @var string - */ - protected $fieldName = 'field_test'; - - /** - * Modules to install. - * - * @var array - */ - public static $modules = array('entity_reference', 'entity_reference_test'); - - protected function setUp() { - parent::setUp(); - - entity_reference_create_field($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType); - } - - /** * Tests autocompletion edge cases with slashes in the names. */ function testEntityReferenceAutocompletion() { // Add an entity with a slash in its name. - $entity_1 = entity_create($this->entityType, array('name' => '10/16/2011', $this->fieldName => NULL)); + $entity_1 = entity_create($this->entityType, array('name' => '10/16/2011')); $entity_1->save(); // Add another entity that differs after the slash character. - $entity_2 = entity_create($this->entityType, array('name' => '10/17/2011', $this->fieldName => NULL)); + $entity_2 = entity_create($this->entityType, array('name' => '10/17/2011')); $entity_2->save(); // Add another entity that has both a comma and a slash character. - $entity_3 = entity_create($this->entityType, array('name' => 'label with, and / test', $this->fieldName => NULL)); + $entity_3 = entity_create($this->entityType, array('name' => 'label with, and / test')); $entity_3->save(); // Try to autocomplete a entity label that matches both entities. // We should get both entities in a JSON encoded string. $input = '10/'; - $data = $this->getAutocompleteResult('single', $input); + $data = $this->getAutocompleteResult($input); $this->assertIdentical($data[0]['label'], String::checkPlain($entity_1->name->value), 'Autocomplete returned the first matching entity'); $this->assertIdentical($data[1]['label'], String::checkPlain($entity_2->name->value), 'Autocomplete returned the second matching entity'); // Try to autocomplete a entity label that matches the first entity. // We should only get the first entity in a JSON encoded string. $input = '10/16'; - $data = $this->getAutocompleteResult('single', $input); + $data = $this->getAutocompleteResult($input); $target = array( 'value' => $entity_1->name->value . ' (1)', 'label' => String::checkPlain($entity_1->name->value), @@ -92,12 +70,12 @@ function testEntityReferenceAutocompletion() { // Try to autocomplete a entity label that matches the second entity, and // the first entity is already typed in the autocomplete (tags) widget. $input = $entity_1->name->value . ' (1), 10/17'; - $data = $this->getAutocompleteResult('tags', $input); + $data = $this->getAutocompleteResult($input); $this->assertIdentical($data[0]['label'], String::checkPlain($entity_2->name->value), 'Autocomplete returned the second matching entity'); // Try to autocomplete a entity label with both a comma and a slash. $input = '"label with, and / t'; - $data = $this->getAutocompleteResult('single', $input); + $data = $this->getAutocompleteResult($input); $n = $entity_3->name->value . ' (3)'; // Entity labels containing commas or quotes must be wrapped in quotes. $n = Tags::encode($n); @@ -111,52 +89,20 @@ function testEntityReferenceAutocompletion() { /** * Returns the result of an Entity reference autocomplete request. * - * @param string $type - * The Entity reference autocomplete type (e.g. 'single', 'tags'). * @param string $input * The label of the entity to query by. * * @return mixed * The JSON value encoded in its appropriate PHP type. */ - protected function getAutocompleteResult($type, $input) { - $request = Request::create('entity_reference/autocomplete/' . $type . '/' . $this->fieldName . '/node/article/NULL'); + protected function getAutocompleteResult($input) { + $request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default'); $request->query->set('q', $input); - $entity_reference_controller = EntityReferenceController::create($this->container); - $result = $entity_reference_controller->handleAutocomplete($request, $type, $this->fieldName, $this->entityType, $this->bundle, 'NULL')->getContent(); + $entity_reference_controller = EntityAutocompleteController::create($this->container); + $result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default')->getContent(); return Json::decode($result); } - /** - * Tests autocomplete for entity base fields. - */ - public function testBaseField() { - // Add two users. - $user_1 = entity_create('user', array('name' => 'auto1', 'status' => TRUE)); - $user_1->save(); - $user_2 = entity_create('user', array('name' => 'auto2', 'status' => TRUE)); - $user_2->save(); - - $request = Request::create('entity_reference/autocomplete/single/user_id/entity_test/entity_test/NULL'); - $request->query->set('q', 'auto'); - - $entity_reference_controller = EntityReferenceController::create($this->container); - $result = $entity_reference_controller->handleAutocomplete($request, 'single', 'user_id', 'entity_test', 'entity_test', 'NULL')->getContent(); - - $data = Json::decode($result); - $this->assertIdentical($data[0]['label'], String::checkPlain($user_1->getUsername()), 'Autocomplete returned the first matching entity'); - $this->assertIdentical($data[1]['label'], String::checkPlain($user_2->getUsername()), 'Autocomplete returned the second matching entity'); - - // Checks that exception thrown for unknown field. - try { - $entity_reference_controller->handleAutocomplete($request, 'single', 'unknown_field', 'entity_test', 'entity_test', 'NULL')->getContent(); - $this->fail('Autocomplete throws exception for unknown field.'); - } - catch (AccessDeniedHttpException $e) { - $this->pass('Autocomplete throws exception for unknown field.'); - } - } - } diff --git a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php index e0e37f3..4a56198 100644 --- a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php +++ b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php @@ -18,7 +18,19 @@ */ class FieldWidgetConstraintValidatorTest extends KernelTestBase { - public static $modules = array('entity_test', 'field', 'user'); + public static $modules = array('entity_test', 'field', 'user', 'system'); + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('system', 'router'); + $this->container->get('router.builder')->rebuild(); + + $this->installEntitySchema('user'); + } /** * Tests widget constraint validation. @@ -37,6 +49,8 @@ public function testValidation() { \Drupal::formBuilder()->processForm('field_test_entity_form', $form, $form_state); // Validate the field constraint. + $form_state->getFormObject()->setEntity($entity)->setFormDisplay($display, $form_state); + $entity = $form_state->getFormObject()->buildEntity($form, $form_state); $display->validateFormValues($entity, $form, $form_state); $errors = $form_state->getErrors(); diff --git a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php index fff190b..1755091 100644 --- a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php @@ -187,7 +187,7 @@ function testBreadCrumbs() { $menu = 'tools'; $edit = array( 'title[0][value]' => 'Root', - 'link[0][uri]' => 'node', + 'link[0][uri]' => '/node', ); $this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save')); $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => 'Root')); @@ -240,7 +240,7 @@ function testBreadCrumbs() { $term = $data['term']; $edit = array( 'title[0][value]' => "$name link", - 'link[0][uri]' => "taxonomy/term/{$term->id()}", + 'link[0][uri]' => "/taxonomy/term/{$term->id()}", 'menu_parent' => "$menu:{$parent_mlid}", 'enabled[value]' => 1, ); @@ -248,7 +248,7 @@ function testBreadCrumbs() { $menu_links = entity_load_multiple_by_properties('menu_link_content', array( 'title' => $edit['title[0][value]'], // @todo Use link.uri once https://www.drupal.org/node/2391217 is in. - 'link__uri' => 'user-path:taxonomy/term/' . $term->id(), + 'link__uri' => 'user-path:/taxonomy/term/' . $term->id(), )); $tags[$name]['link'] = reset($menu_links); $parent_mlid = $tags[$name]['link']->getPluginId(); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 2bb9e62..7c87bb9 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -460,3 +460,11 @@ system.admin_content: _title: 'Content' requirements: _permission: 'access administration pages' + +system.entity_autocomplete: + path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings}' + defaults: + _controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete' + selection_settings: '' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index 3b9309d..0d6234d 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -115,7 +115,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'settings' => array( 'match_operator' => 'CONTAINS', 'size' => '60', - 'autocomplete_type' => 'tags', 'placeholder' => '', ), )); diff --git a/core/modules/user/src/Tests/UserEntityReferenceTest.php b/core/modules/user/src/Tests/UserEntityReferenceTest.php index b511203..215bf98 100644 --- a/core/modules/user/src/Tests/UserEntityReferenceTest.php +++ b/core/modules/user/src/Tests/UserEntityReferenceTest.php @@ -79,10 +79,10 @@ function testUserSelectionByRole() { $user3->save(); - /** @var \Drupal\entity_reference\EntityReferenceAutocomplete $autocomplete */ - $autocomplete = \Drupal::service('entity_reference.autocomplete'); + /** @var \Drupal\Core\Entity\EntityAutocompleteMatcher $autocomplete */ + $autocomplete = \Drupal::service('entity.autocomplete_matcher'); - $matches = $autocomplete->getMatches($field_definition, 'user', 'user', 'NULL', '', 'aabb'); + $matches = $autocomplete->getMatches('user', 'default', $field_definition->getSetting('handler_settings'), 'aabb'); $this->assertEqual(count($matches), 2); $users = array(); foreach ($matches as $match) { @@ -92,7 +92,7 @@ function testUserSelectionByRole() { $this->assertTrue(in_array($user2->label(), $users)); $this->assertFalse(in_array($user3->label(), $users)); - $matches = $autocomplete->getMatches($field_definition, 'user', 'user', 'NULL', '', 'aabbbb'); + $matches = $autocomplete->getMatches('user', 'default', $field_definition->getSetting('handler_settings'), 'aabbbb'); $this->assertEqual(count($matches), 0, ''); } } diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml index a121cf2..9e54f2a 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml @@ -52,7 +52,6 @@ content: settings: match_operator: CONTAINS size: 60 - autocomplete_type: tags placeholder: '' third_party_settings: { } created: diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml index 32ffd5b..967c74e 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml @@ -26,7 +26,6 @@ content: settings: match_operator: CONTAINS size: 60 - autocomplete_type: tags placeholder: '' third_party_settings: { } created: