Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
AutocompleteController
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
3 / 3
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 autocomplete
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Controller;
6
7use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
8use Drupal\Core\Entity\EntityFieldManager;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\name\Service\AutocompleteInterface;
11use Symfony\Component\DependencyInjection\ContainerInterface;
12use Symfony\Component\HttpFoundation\JsonResponse;
13use Symfony\Component\HttpFoundation\Request;
14use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
15
16/**
17 * Controller routines for name autocompletion routes.
18 */
19class AutocompleteController implements ContainerInjectionInterface {
20
21  /**
22   * The name autocomplete helper class to find matching name values.
23   *
24   * @var \Drupal\name\Service\AutocompleteInterface
25   */
26  protected AutocompleteInterface $nameAutocomplete;
27
28  /**
29   * Entity field manager.
30   *
31   * @var \Drupal\Core\Entity\EntityFieldManager
32   */
33  protected $entityFieldManager;
34
35  /**
36   * Entity type manager.
37   *
38   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
39   */
40  protected $entityTypeManager;
41
42  /**
43   * Constructs an AutocompleteController object.
44   *
45   * @param \Drupal\name\Service\AutocompleteInterface $name_autocomplete
46   *   The name autocomplete helper class to find matching name values.
47   * @param \Drupal\Core\Entity\EntityFieldManager $entityFieldManager
48   *   The entity field manager.
49   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
50   *   The entity field manager.
51   */
52  public function __construct(AutocompleteInterface $name_autocomplete, EntityFieldManager $entityFieldManager, EntityTypeManagerInterface $entityTypeManager) {
53    $this->nameAutocomplete = $name_autocomplete;
54    $this->entityFieldManager = $entityFieldManager;
55    $this->entityTypeManager = $entityTypeManager;
56  }
57
58  /**
59   * {@inheritdoc}
60   */
61  public static function create(ContainerInterface $container) {
62    return new static(
63      $container->get('name.autocomplete'),
64      $container->get('entity_field.manager'),
65      $container->get('entity_type.manager')
66    );
67  }
68
69  /**
70   * Returns response for the name autocompletion.
71   *
72   * @param \Symfony\Component\HttpFoundation\Request $request
73   *   The current request object containing the search string.
74   * @param string $field_name
75   *   The field name.
76   * @param string $entity_type
77   *   The entity type.
78   * @param string $bundle
79   *   The bundle.
80   * @param string $component
81   *   The name component.
82   *
83   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
84   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
85   *
86   * @return \Symfony\Component\HttpFoundation\JsonResponse
87   *   A JSON response containing the autocomplete suggestions.
88   *
89   * @see \Drupal\name\Service\AutocompleteInterface::getMatches()
90   */
91  public function autocomplete(Request $request, $field_name, $entity_type, $bundle, $component) {
92    $definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle);
93
94    if (!isset($definitions[$field_name])) {
95      throw new AccessDeniedHttpException();
96    }
97
98    $field_definition = $definitions[$field_name];
99    if ($field_definition->getType() != 'name') {
100      throw new AccessDeniedHttpException();
101    }
102
103    $access_handler = $this->entityTypeManager->getAccessControlHandler($entity_type);
104    // A bare fieldAccess('edit') call without an entity falls back to "allow"
105    // for most content entity field access handlers, which would leak stored
106    // field values to any authenticated user with 'access content'. Gate the
107    // endpoint on an actual ability to use the field: the user must be able
108    // to create a new entity of this bundle (or edit any of them via bypass
109    // permissions) before we enumerate stored values.
110    if (!$access_handler->createAccess($bundle)) {
111      throw new AccessDeniedHttpException();
112    }
113    if (!$access_handler->fieldAccess('edit', $field_definition)) {
114      throw new AccessDeniedHttpException();
115    }
116
117    $matches = $this->nameAutocomplete->getMatches($field_definition, (string) $component, (string) $request->query->get('q'));
118    // Core's autocomplete JS (jQuery UI) expects a list of {value, label}
119    // objects, not an associative value => label map.
120    $results = [];
121    foreach ($matches as $value => $label) {
122      $results[] = [
123        'value' => (string) $value,
124        'label' => (string) $label,
125      ];
126    }
127    $response = new JsonResponse($results);
128    // Results vary per user (entity view access) and must not be cached by
129    // intermediaries or browsers.
130    $response->setPrivate();
131    $response->headers->addCacheControlDirective('no-store');
132    $response->headers->set('Vary', 'Cookie');
133    return $response;
134  }
135
136}