Commit ade57a9e authored by alexpott's avatar alexpott

Issue #2419923 by amateescu, jibran, dashaforbes: Port SA-CONTRIB-2013-096 to D8

parent 8007ab1b
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity\Element;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Textfield;
use Drupal\user\EntityOwnerInterface;
......@@ -15,6 +16,9 @@
/**
* Provides an entity autocomplete form element.
*
* The #default_value accepted by this element is either an entity object or an
* array of entity objects.
*
* @FormElement("entity_autocomplete")
*/
class EntityAutocomplete extends Textfield {
......@@ -35,17 +39,42 @@ public function getInfo() {
// This should only be set to FALSE if proper validation by the selection
// handler is performed at another level on the extracted form values.
$info['#validate_reference'] = TRUE;
// IMPORTANT! This should only be set to FALSE if the #default_value
// property is processed at another level (e.g. by a Field API widget) and
// it's value is properly checked for access.
$info['#process_default_value'] = TRUE;
$info['#element_validate'] = array(array($class, 'validateEntityAutocomplete'));
array_unshift($info['#process'], array($class, 'processEntityAutocomplete'));
// @todo Consider providing better DX for #default_value? Maybe we impose an
// array('label' => .., 'value' => ..) structure instead of manually
// composing the textfield string?. See https://www.drupal.org/node/2418249.
return $info;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
// Process the #default_value property.
if ($input === FALSE && isset($element['#default_value']) && $element['#process_default_value']) {
if (is_array($element['#default_value']) && $element['#tags'] !== TRUE) {
throw new \InvalidArgumentException('The #default_value property is an array but the form element does not allow multiple values.');
}
elseif (!is_array($element['#default_value'])) {
// Convert the default value into an array for easier processing in
// static::getEntityLabels().
$element['#default_value'] = array($element['#default_value']);
}
if ($element['#default_value'] && !(reset($element['#default_value']) instanceof EntityInterface)) {
throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.');
}
// Extract the labels from the passed-in entity objects, taking access
// checks into account.
return static::getEntityLabels($element['#default_value']);
}
}
/**
* Adds entity autocomplete functionality to a form element.
*
......@@ -159,6 +188,35 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte
$form_state->setValueForElement($element, $value);
}
/**
* Converts an array of entity objects into a string of entity labels.
*
* This method is also responsible for checking the 'view' access on the
* passed-in entities.
*
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entity objects.
*
* @return string
* A string of entity labels separated by commas.
*/
public static function getEntityLabels(array $entities) {
$entity_labels = array();
foreach ($entities as $entity) {
$label = ($entity->access('view')) ? $entity->label() : t('- Restricted access -');
// Take into account "autocreated" entities.
if (!$entity->isNew()) {
$label .= ' (' . $entity->id() . ')';
}
// Labels containing commas or quotes must be wrapped in quotes.
$entity_labels[] = Tags::encode($label);
}
return implode(', ', $entity_labels);
}
/**
* Extracts the entity ID from the autocompletion result.
*
......
......@@ -32,6 +32,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$element['target_id']['#tags'] = TRUE;
$element['target_id']['#default_value'] = $items->referencedEntities();
return $element;
}
......
......@@ -7,8 +7,6 @@
namespace Drupal\Core\Field\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
......@@ -94,6 +92,7 @@ public function settingsSummary() {
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity = $items->getEntity();
$referenced_entities = $items->referencedEntities();
$element += array(
'#type' => 'entity_autocomplete',
......@@ -104,7 +103,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
// the 'ValidReference' constraint.
'#validate_reference' => FALSE,
'#maxlength' => 1024,
'#default_value' => implode(', ', $this->getLabels($items, $delta)),
'#default_value' => isset($referenced_entities[$delta]) ? $referenced_entities[$delta] : NULL,
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
);
......@@ -142,35 +141,6 @@ public function massageFormValues(array $values, array $form, FormStateInterface
return $values;
}
/**
* Gets the entity labels.
*/
protected function getLabels(EntityReferenceFieldItemListInterface $items, $delta) {
if ($items->isEmpty()) {
return array();
}
$entity_labels = array();
$handles_multiple_values = $this->handlesMultipleValues();
foreach ($items->referencedEntities() as $referenced_delta => $referenced_entity) {
// The autocomplete widget outputs one entity label per form element.
if (!$handles_multiple_values && $referenced_delta != $delta) {
continue;
}
$key = $referenced_entity->label();
// Take into account "autocreate" items.
if (!$referenced_entity->isNew()) {
$key .= ' (' . $referenced_entity->id() . ')';
}
// Labels containing commas or quotes must be wrapped in quotes.
$entity_labels[] = Tags::encode($key);
}
return $entity_labels;
}
/**
* Returns the name of the bundle which will be used for autocreated entities.
*
......
......@@ -55,7 +55,7 @@ protected function setUp() {
parent::setUp();
// Create a test user.
$web_user = $this->drupalCreateUser(array('administer entity_test content', 'administer entity_test fields'));
$web_user = $this->drupalCreateUser(array('administer entity_test content', 'administer entity_test fields', 'view test entity'));
$this->drupalLogin($web_user);
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\link\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Field\FieldItemListInterface;
......@@ -17,7 +16,6 @@
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;
/**
......@@ -81,17 +79,10 @@ protected static function getUriAsDisplayableString($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).
// Show the 'entity:' URI as the entity autocomplete would.
$entity_manager = \Drupal::entityManager();
if ($entity_manager->getDefinition($entity_type, FALSE)) {
$entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id);
if ($entity) {
$label = ($entity->access('view')) ? $entity->label() : t('- Restricted access -');
$displayable_string = $label . ' (' . $entity_id . ')';
}
if ($entity_manager->getDefinition($entity_type, FALSE) && $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id)) {
$displayable_string = EntityAutocomplete::getEntityLabels(array($entity));
}
}
......@@ -215,6 +206,10 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
$element['uri']['#target_type'] = 'node';
// Disable autocompletion when the first character is '/', '#' or '?'.
$element['uri']['#attributes']['data-autocomplete-first-character-blacklist'] = '/#?';
// The link widget is doing its own processing in
// static::getUriAsDisplayableString().
$element['uri']['#process_default_value'] = FALSE;
}
// If the field is configured to allow only internal links, add a useful
......
......@@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Entity\Element;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
......@@ -141,6 +142,18 @@ public function buildForm(array $form, FormStateInterface $form_state) {
),
);
$form['single_access'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#default_value' => $this->referencedEntities[0],
);
$form['tags_access'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#tags' => TRUE,
'#default_value' => array($this->referencedEntities[0], $this->referencedEntities[1]),
);
return $form;
}
......@@ -274,6 +287,32 @@ public function testInvalidEntityAutocompleteElement() {
$this->assertEqual(count($form_state->getErrors()), 0);
}
/**
* Tests that access is properly checked by the EntityAutocomplete element.
*/
public function testEntityAutocompleteAccess() {
$form_builder = $this->container->get('form_builder');
$form = $form_builder->getForm($this);
// Check that the current user has proper access to view entity labels.
$expected = $this->referencedEntities[0]->label() . ' (' . $this->referencedEntities[0]->id() . ')';
$this->assertEqual($form['single_access']['#value'], $expected);
$expected .= ', ' . $this->referencedEntities[1]->label() . ' (' . $this->referencedEntities[1]->id() . ')';
$this->assertEqual($form['tags_access']['#value'], $expected);
// Set up a non-admin user that is *not* allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(array(), array()));
// Rebuild the form.
$form = $form_builder->getForm($this);
$expected = t('- Restricted access -') . ' (' . $this->referencedEntities[0]->id() . ')';
$this->assertEqual($form['single_access']['#value'], $expected);
$expected .= ', ' . t('- Restricted access -') . ' (' . $this->referencedEntities[1]->id() . ')';
$this->assertEqual($form['tags_access']['#value'], $expected);
}
/**
* Returns an entity label in the format needed by the EntityAutocomplete
......@@ -286,7 +325,7 @@ public function testInvalidEntityAutocompleteElement() {
* A string that can be used as a value for EntityAutocomplete elements.
*/
protected function getAutocompleteInput(EntityInterface $entity) {
return $entity->label() . ' (' . $entity->id() . ')';
return EntityAutocomplete::getEntityLabels(array($entity));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment