diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 4f2e23b980b944e23dd2c4bea376c716dee8229a..eed8739ddc891026b3a5df2c380347f33a0ecae6 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -7,10 +7,12 @@ namespace Drupal\Core\Entity\Element; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Tags; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\Textfield; +use Drupal\Core\Site\Settings; use Drupal\user\EntityOwnerInterface; /** @@ -112,11 +114,22 @@ public static function processEntityAutocomplete(array &$element, FormStateInter $element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id(); } + // Store the selection settings in the key/value store and pass a hashed key + // in the route parameters. + $selection_settings = isset($element['#selection_settings']) ? $element['#selection_settings'] : []; + $data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler']; + $selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt()); + + $key_value_storage = \Drupal::keyValue('entity_autocomplete'); + if (!$key_value_storage->has($selection_settings_key)) { + $key_value_storage->set($selection_settings_key, $selection_settings); + } + $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'])) : '', + 'selection_settings_key' => $selection_settings_key, ); return $element; diff --git a/core/modules/field/src/Tests/FieldUnitTestBase.php b/core/modules/field/src/Tests/FieldUnitTestBase.php index 3c2b53546207999b8d72caaed3fec1279dc332d5..9587a4a792d05df015483307b02325a605a6a819 100644 --- a/core/modules/field/src/Tests/FieldUnitTestBase.php +++ b/core/modules/field/src/Tests/FieldUnitTestBase.php @@ -50,8 +50,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('user'); - $this->installSchema('system', array('sequences')); - $this->installSchema('system', array('router')); + $this->installSchema('system', ['router', 'sequences', 'key_value']); // Set default storage backend and configure the theme system. $this->installConfig(array('field', 'system')); diff --git a/core/modules/system/src/Controller/EntityAutocompleteController.php b/core/modules/system/src/Controller/EntityAutocompleteController.php index 3da9b853bf9d443f37e439bfe09a86c1968a2e04..44b6596ffd520566a1cf5b0c80c572fefda63118 100644 --- a/core/modules/system/src/Controller/EntityAutocompleteController.php +++ b/core/modules/system/src/Controller/EntityAutocompleteController.php @@ -7,13 +7,17 @@ namespace Drupal\system\Controller; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Tags; use Drupal\Component\Utility\Unicode; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityAutocompleteMatcher; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Defines a route controller for entity autocomplete form elements. @@ -27,14 +31,24 @@ class EntityAutocompleteController extends ControllerBase { */ protected $matcher; + /** + * The key value store. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $keyValue; + /** * Constructs a EntityAutocompleteController object. * * @param \Drupal\Core\Entity\EntityAutocompleteMatcher $matcher * The autocomplete matcher for entity references. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value + * The key value factory. */ - public function __construct(EntityAutocompleteMatcher $matcher) { + public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) { $this->matcher = $matcher; + $this->keyValue = $key_value; } /** @@ -42,7 +56,8 @@ public function __construct(EntityAutocompleteMatcher $matcher) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity.autocomplete_matcher') + $container->get('entity.autocomplete_matcher'), + $container->get('keyvalue')->get('entity_autocomplete') ); } @@ -55,21 +70,40 @@ public static function create(ContainerInterface $container) { * 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. + * @param string $selection_settings_key + * The hashed key of the key/value entry that holds the selection handler + * settings. * * @return \Symfony\Component\HttpFoundation\JsonResponse * The matched entity labels as a JSON response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown if the selection settings key is not found in the key/value store + * or if it does not match the stored data. */ - public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings = '') { + public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings_key) { $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(); + // Selection settings are passed in as a hashed key of a serialized array + // stored in the key/value store. + $selection_settings = $this->keyValue->get($selection_settings_key, FALSE); + if ($selection_settings !== FALSE) { + $selection_settings_hash = Crypt::hmacBase64(serialize($selection_settings) . $target_type . $selection_handler, Settings::getHashSalt()); + if ($selection_settings_hash !== $selection_settings_key) { + // Disallow access when the selection settings hash does not match the + // passed-in key. + throw new AccessDeniedHttpException('Invalid selection settings key.'); + } + } + else { + // Disallow access when the selection settings key is not found in the + // key/value store. + throw new AccessDeniedHttpException(); + } $matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string); } diff --git a/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php b/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php index 949b7ccf37d4013aa45d7b4fa3bc8587492ef953..d1425c304d3f038fe1cd8ba9122fd01895513403 100644 --- a/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php +++ b/core/modules/system/src/Tests/Entity/EntityAutocompleteTest.php @@ -8,10 +8,13 @@ namespace Drupal\system\Tests\Entity; use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Tags; +use Drupal\Core\Site\Settings; use Drupal\system\Controller\EntityAutocompleteController; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Tests the autocomplete functionality. @@ -34,6 +37,14 @@ class EntityAutocompleteTest extends EntityUnitTestBase { */ protected $bundle = 'entity_test'; + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installSchema('system', ['key_value']); + } + /** * Tests autocompletion edge cases with slashes in the names. */ @@ -86,6 +97,47 @@ function testEntityReferenceAutocompletion() { $this->assertIdentical(reset($data), $target, 'Autocomplete returns an entity label containing a comma and a slash.'); } + /** + * Tests that missing or invalid selection setting key are handled correctly. + */ + public function testSelectionSettingsHandling() { + $entity_reference_controller = EntityAutocompleteController::create($this->container); + $request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default'); + $request->query->set('q', $this->randomString()); + + try { + // Pass an invalid selection settings key (i.e. one that does not exist + // in the key/value store). + $selection_settings_key = $this->randomString(); + $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key); + + $this->fail('Non-existent selection settings key throws an exception.'); + } + catch (AccessDeniedHttpException $e) { + $this->pass('Non-existent selection settings key throws an exception.'); + } + + try { + // Generate a valid hash key but store a modified settings array. + $selection_settings = []; + $selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt()); + + $selection_settings[$this->randomMachineName()] = $this->randomString(); + \Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings); + + $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key); + } + catch (AccessDeniedHttpException $e) { + if ($e->getMessage() == 'Invalid selection settings key.') { + $this->pass('Invalid selection settings key throws an exception.'); + } + else { + $this->fail('Invalid selection settings key throws an exception.'); + } + } + + } + /** * Returns the result of an Entity reference autocomplete request. * @@ -99,8 +151,12 @@ protected function getAutocompleteResult($input) { $request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default'); $request->query->set('q', $input); + $selection_settings = []; + $selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt()); + \Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings); + $entity_reference_controller = EntityAutocompleteController::create($this->container); - $result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default')->getContent(); + $result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key)->getContent(); return Json::decode($result); } diff --git a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php index 4a561987525697c930ef129c2159e314ceebeeef..0ed3ec7086f5e629a0aded375c60751486e3f86a 100644 --- a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php +++ b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php @@ -26,7 +26,7 @@ class FieldWidgetConstraintValidatorTest extends KernelTestBase { protected function setUp() { parent::setUp(); - $this->installSchema('system', 'router'); + $this->installSchema('system', ['router', 'key_value']); $this->container->get('router.builder')->rebuild(); $this->installEntitySchema('user'); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 661f4e00300f0cfee5367ddf3fa7459b4a8b178b..12e7d9accdc44b3a7d611a77089d38aeba54282d 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -476,9 +476,8 @@ system.admin_content: _permission: 'access administration pages' system.entity_autocomplete: - path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings}' + path: '/entity_reference_autocomplete/{target_type}/{selection_handler}/{selection_settings_key}' defaults: _controller: '\Drupal\system\Controller\EntityAutocompleteController::handleAutocomplete' - selection_settings: '' requirements: _access: 'TRUE'