diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index 2d1b7dc..8fa60f8 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.string: 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..27d814f --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -0,0 +1,196 @@ + .., 'value' => ..) structure instead of manually + // composing the textfield string? + + 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(&$element, FormStateInterface $form_state, &$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'] = $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(&$element, FormStateInterface $form_state, &$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) { + // Take "label (entity id)', match the ID from parenthesis when it's a + // number. + $match = static::extractEntityIdFormAutocompletionResult($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 + // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). + $value[] = array( + 'entity' => static::createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']) + ); + } + } + + // 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 + * NULL if no entity could be extracted, otherwise the entity ID. + */ + public static function extractEntityIdFormAutocompletionResult($input) { + $match = NULL; + 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/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php new file mode 100644 index 0000000..859f613 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php @@ -0,0 +1,89 @@ +selectionHandlerManager = $selection_manager; + } + + /** + * Returns matched labels based on a given search string. + * + * @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. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the current user doesn't have access to the specifies entity. + * + * @return array + * An array of matched entity labels, in the format required by the AJAX + * autocomplete API (e.g. array('value' => $value, 'label' => $label)). + * + * @see \Drupal\system\Controller\EntityAutocompleteController + */ + public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') { + $matches = array(); + + $options = array( + 'target_type' => $target_type, + 'handler' => $selection_handler, + 'handler_settings' => $selection_settings, + ); + $handler = $this->selectionHandlerManager->getInstance($options); + + if (isset($string)) { + // Get an array of matching entities. + $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. + foreach ($entity_labels as $values) { + foreach ($values as $entity_id => $label) { + $key = "$label ($entity_id)"; + // Strip things like starting/trailing white spaces, line breaks and + // tags. + $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' => $key, 'label' => $label); + } + } + } + + return $matches; + } + +} diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php new file mode 100644 index 0000000..268d10d --- /dev/null +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php @@ -0,0 +1,46 @@ + 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ) + parent::defaultSettings(); + } /** * {@inheritdoc} @@ -75,30 +95,24 @@ 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'), '#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 +124,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 +169,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 +208,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/Path/PathValidator.php b/core/lib/Drupal/Core/Path/PathValidator.php index 51cdc2f..fcf0968 100644 --- a/core/lib/Drupal/Core/Path/PathValidator.php +++ b/core/lib/Drupal/Core/Path/PathValidator.php @@ -109,6 +109,9 @@ protected function getUrl($path, $access_check) { if ($parsed_url['path'] == '') { return new Url('', [], $options); } + elseif ($parsed_url['path'] == '') { + return new Url('', [], $options); + } elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) { if (empty($parsed_url['path'])) { return FALSE; 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/EntityReferenceAutocomplete.php b/core/modules/entity_reference/src/EntityReferenceAutocomplete.php deleted file mode 100644 index f0a6157..0000000 --- a/core/modules/entity_reference/src/EntityReferenceAutocomplete.php +++ /dev/null @@ -1,112 +0,0 @@ -entityManager = $entity_manager; - $this->selectionHandlerManager = $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 $string - * (optional) The label of the entity to query by. - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * Thrown when the current user doesn't have access to the specifies entity. - * - * @return array - * A list of matched entity labels. - * - * @see \Drupal\entity_reference\EntityReferenceController - */ - public function getMatches(FieldDefinitionInterface $field_definition, $entity_type, $bundle, $entity_id = '', $prefix = '', $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); - - 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'; - $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10); - - // Loop through the entities and convert them into autocomplete output. - foreach ($entity_labels as $values) { - foreach ($values as $entity_id => $label) { - $key = "$label ($entity_id)"; - // Strip things like starting/trailing white spaces, line breaks and - // tags. - $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); - } - } - } - - return $matches; - } - -} diff --git a/core/modules/entity_reference/src/EntityReferenceController.php b/core/modules/entity_reference/src/EntityReferenceController.php deleted file mode 100644 index afc4cc0..0000000 --- a/core/modules/entity_reference/src/EntityReferenceController.php +++ /dev/null @@ -1,102 +0,0 @@ -entityReferenceAutocomplete = $entity_reference_autocompletion; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_reference.autocomplete') - ); - } - - /** - * Autocomplete the label of an entity. - * - * @param 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. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * The matched labels as json. - */ - public function handleAutocomplete(Request $request, $type, $field_name, $entity_type, $bundle_name, $entity_id) { - $definitions = $this->entityManager()->getFieldDefinitions($entity_type, $bundle_name); - - if (!isset($definitions[$field_name])) { - throw new AccessDeniedHttpException(); - } - - $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(); - } - - // 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/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php b/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.php deleted file mode 100644 index f7b5ff3..0000000 --- a/core/modules/entity_reference/src/Plugin/Field/FieldWidget/AutocompleteTagsWidget.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) { - $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); - } -} 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 0606f42..b6bb75d 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -9,6 +9,7 @@ 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; @@ -44,15 +45,29 @@ 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); + $uri = str_replace('', '', $uri); + $uri = str_replace('', '/', $uri); + if ($scheme === 'user-path') { - $uri_reference = explode(':', $uri, 2)[1]; + $uri_reference = ltrim(explode(':', $uri, 2)[1], '/'); + if ($uri_reference && !in_array($uri_reference[0], ['?', '#', '/'])) { + $uri_reference = '/' . $uri_reference; + } + } + elseif ($scheme === 'entity') { + list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); + $uri_reference = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id)->label() . ' (' . $entity_id . ')'; } else { $uri_reference = $uri; @@ -65,18 +80,33 @@ 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::extractEntityIdFormAutocompletionResult($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; + if (parse_url($string, PHP_URL_PATH) === NULL) { + $string = '' . $string; + } + if (parse_url($string, PHP_URL_PATH) === '/') { + $string = '' . ltrim($string, '/'); + } + $string = 'user-path:' . $string; } } return $string; @@ -87,6 +117,15 @@ protected static function getUserEnteredStringAsUri($string) { */ public static function validateUriElement($element, FormStateInterface $form_state, $form) { $uri = static::getUserEnteredStringAsUri($element['#value']); + $form_state->setValueForElement($element, $uri); + + // Figure out the user entered value after user-path. This might contain a + // fragment so we cannot use parse_url($string, PHP_URL_PATH), so we strip + // out the 10 chars of user-path:. + if (parse_url($uri, PHP_URL_SCHEME) === 'user-path' && !in_array(substr($uri, 10)[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. @@ -131,18 +170,20 @@ 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'; } // 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..7fed262 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -98,9 +98,11 @@ function testURLValidation() { 'http://www.example.com/', ); $valid_internal_entries = array( - 'entity_test/add', - 'a/path/alias', + '/entity_test/add', + '/a/path/alias', 'entity:user/1', + '#example', + '?example=llama', ); // Define some invalid URLs. @@ -113,7 +115,10 @@ function testURLValidation() { 'http://', ); $invalid_internal_entries = array( - 'non/existing/path', + // No existing. + '/non/existing/path', + // Missing start with / + 'no-slash-start', ); // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. 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..120ff28 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -106,7 +106,7 @@ function testMenu() { foreach ($this->items as $item) { // Paths were set as 'node/$nid'. - $node = Node::load(str_replace('user-path:node/', '', $item->link->uri)); + $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,21 @@ 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'; + $path = '/?arg1=value#fragment'; $item = $this->addMenuLink('', $path); $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); @@ -544,7 +544,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 +561,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 +603,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 +637,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..89962b2 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:' . ($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, 'link[0][uri]' => str_replace('user-path:', '', $shortcut->link->uri)), 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,7 +150,7 @@ 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); diff --git a/core/modules/system/src/Controller/EntityAutocompleteController.php b/core/modules/system/src/Controller/EntityAutocompleteController.php new file mode 100644 index 0000000..bd47031 --- /dev/null +++ b/core/modules/system/src/Controller/EntityAutocompleteController.php @@ -0,0 +1,80 @@ +entityAutocompleteMatcher = $entity_autocomplete_matcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.autocomplete_matcher') + ); + } + + /** + * Autocomplete the label of an entity. + * + * @param Request $request + * The request object that contains the typed tags. + * @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 entity labels as a JSON response. + */ + 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)); + + // Selection settings are passed in as an encoded serialized array. + $selection_settings = $selection_settings ? unserialize(base64_decode($selection_settings)) : array(); + + $matches = $this->entityAutocompleteMatcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string); + } + + return new JsonResponse($matches); + } + +} 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..36e9d72 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 */ -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..fecfd249 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -382,7 +382,8 @@ system.theme_settings_theme: path: '' options: _only_fragment: TRUE - + requirements: + _access: 'TRUE' '': path: '' @@ -460,3 +461,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: