Commit 691a5762 authored by alexpott's avatar alexpott

Issue #1994140 by Berdir, yched, fgm, smiletrl: Unify entity field access and Field API access.

parent ef02f36a
......@@ -7,7 +7,9 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Field\FieldItemListInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Entity\Field\FieldDefinitionInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
......@@ -268,4 +270,42 @@ public function setModuleHandler(ModuleHandlerInterface $module_handler) {
return $this;
}
/**
* {@inheritdoc}
*/
public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL) {
$account = $this->prepareUser($account);
// Get the default access restriction that lives within this field.
$default = $items ? $items->defaultAccess($operation, $account) : TRUE;
// Invoke hook and collect grants/denies for field access from other
// modules. Our default access flag is masked under the ':default' key.
$grants = array(':default' => $default);
$hook_implementations = $this->moduleHandler->getImplementations('entity_field_access');
foreach ($hook_implementations as $module) {
$grants = array_merge($grants, array($module => $this->moduleHandler->invoke($module, 'entity_field_access', array($operation, $field_definition, $account, $items))));
}
// Also allow modules to alter the returned grants/denies.
$context = array(
'operation' => $operation,
'field_definition' => $field_definition,
'items' => $items,
'account' => $account,
);
$this->moduleHandler->alter('entity_field_access', $grants, $context);
// One grant being FALSE is enough to deny access immediately.
if (in_array(FALSE, $grants, TRUE)) {
return FALSE;
}
// At least one grant has the explicit opinion to allow access.
if (in_array(TRUE, $grants, TRUE)) {
return TRUE;
}
// All grants are NULL and have no opinion - deny access in that case.
return FALSE;
}
}
......@@ -7,7 +7,9 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Field\FieldItemListInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Entity\Field\FieldDefinitionInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
......@@ -70,4 +72,22 @@ public function resetCache();
*/
public function setModuleHandler(ModuleHandlerInterface $module_handler);
/**
* Checks access to an operation on a given entity field.
*
* @param string $operation
* The operation access should be checked for.
* Usually one of "view" or "edit".
* @param \Drupal\Core\Entity\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user session for which to check access, or NULL to check
* access for the current user. Defaults to NULL.
* @param \Drupal\Core\Entity\Field\FieldItemListInterface $items
* (optional) The field values for which to check access, or NULL if access
* is checked for the field definition, without any specific value
* available. Defaults to NULL.
*/
public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL);
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Field\FieldDefinition.
*/
namespace Drupal\Core\Entity\Field;
use Drupal\Core\Entity\EntityInterface;
/**
* A class for defining entity fields.
*/
class FieldDefinition implements FieldDefinitionInterface {
/**
* The array holding values for all definition keys.
*
* @var array
*/
protected $definition = array();
/**
* Constructs a new FieldDefinition object.
*
* @param array $definition
* (optional) If given, a definition represented as array.
*/
public function __construct(array $definition = array()) {
$this->definition = $definition;
}
/**
* {@inheritdoc}
*/
public function getFieldName() {
return $this->definition['field_name'];
}
/**
* Sets the field name.
*
* @param string $name
* The field name to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldName($name) {
$this->definition['field_name'] = $name;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldType() {
// Cut of the leading field_item: prefix from 'field_item:FIELD_TYPE'.
$parts = explode(':', $this->definition['type']);
return $parts[1];
}
/**
* Sets the field type.
*
* @param string $type
* The field type to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldType($type) {
$this->definition['type'] = 'field_item:' . $type;
return $this;
}
/**
* Sets a field setting.
*
* @param string $type
* The field type to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldSetting($setting_name, $value) {
$this->definition['settings'][$setting_name] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldSettings() {
return $this->definition['settings'];
}
/**
* {@inheritdoc}
*/
public function getFieldSetting($setting_name) {
return isset($this->definition['settings'][$setting_name]) ? $this->definition['settings'][$setting_name] : NULL;
}
/**
* {@inheritdoc}
*/
public function getFieldPropertyNames() {
return array_keys(\Drupal::typedData()->create($this->definition['type'])->getPropertyDefinitions());
}
/**
* {@inheritdoc}
*/
public function isFieldTranslatable() {
return !empty($this->definition['translatable']);
}
/**
* Sets whether the field is translatable.
*
* @param bool $translatable
* Whether the field is translatable.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setTranslatable($translatable) {
$this->definition['translatable'] = $translatable;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldLabel() {
return $this->definition['label'];
}
/**
* Sets the field label.
*
* @param string $label
* The field label to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldLabel($label) {
$this->definition['label'] = $label;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldDescription() {
return $this->definition['description'];
}
/**
* Sets the field label.
*
* @param string $description
* The field label to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldDescription($description) {
$this->definition['description'] = $description;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFieldCardinality() {
// @todo: Allow to control this.
return isset($this->definition['cardinality']) ? $this->definition['cardinality'] : 1;
}
/**
* {@inheritdoc}
*/
public function isFieldRequired() {
return !empty($this->definition['required']);
}
/**
* Sets whether the field is required.
*
* @param bool $required
* TRUE if the field is required, FALSE otherwise.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setFieldRequired($required) {
$this->definition['required'] = $required;
return $this;
}
/**
* Sets constraints for a given field item property.
*
* @param string $name
* The name of the property to set constraints for.
* @param array $constraints
* The constraints to set.
*
* @return \Drupal\Core\Entity\Field\FieldDefinition
* The object itself for chaining.
*/
public function setPropertyConstraints($name, array $constraints) {
$this->definition['item_definition']['constraints']['ComplexData'][$name] = $constraints;
return $this;
}
/**
* {@inheritdoc}
*/
public function isFieldConfigurable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getFieldDefaultValue(EntityInterface $entity) {
return $this->getFieldSetting('default_value');
}
}
......@@ -68,10 +68,9 @@ public function getFieldName();
* Returns the field type.
*
* @return string
* The field type.
* The field type, i.e. the id of a field type plugin. For example 'text'.
*
* @todo Provide more information about field types after
* https://drupal.org/node/1969728 is implemented.
* @see \Drupal\Core\Entity\Field\FieldTypePluginManager
*/
public function getFieldType();
......@@ -122,6 +121,13 @@ public function getFieldPropertyNames();
*/
public function isFieldTranslatable();
/**
* Determines whether the field is configurable via field.module.
*
* @return bool
*/
public function isFieldConfigurable();
/**
* Returns the human-readable label for the field.
*
......
......@@ -47,6 +47,7 @@ class FieldItemList extends ItemList implements FieldItemListInterface {
*/
public function __construct(array $definition, $name = NULL, TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
$this->definition['field_name'] = $name;
// Always initialize one empty item as most times a value for at least one
// item will be present. That way prototypes created by
// \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance() will
......@@ -79,8 +80,7 @@ public function getLangcode() {
* {@inheritdoc}
*/
public function getFieldDefinition() {
// @todo https://drupal.org/node/1988612
return NULL;
return new FieldDefinition($this->definition);
}
/**
......@@ -195,52 +195,15 @@ public function __unset($property_name) {
}
/**
* Implements \Drupal\Core\TypedData\AccessibleInterface::access().
* {@inheritdoc}
*/
public function access($operation = 'view', AccountInterface $account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
// Get the default access restriction that lives within this field.
$access = $this->defaultAccess($operation, $account);
// Invoke hook and collect grants/denies for field access from other
// modules. Our default access flag is masked under the ':default' key.
$grants = array(':default' => $access);
$hook_implementations = \Drupal::moduleHandler()->getImplementations('entity_field_access');
foreach ($hook_implementations as $module) {
$grants = array_merge($grants, array($module => module_invoke($module, 'entity_field_access', $operation, $this, $account)));
}
// Also allow modules to alter the returned grants/denies.
$context = array(
'operation' => $operation,
'field' => $this,
'account' => $account,
);
drupal_alter('entity_field_access', $grants, $context);
// One grant being FALSE is enough to deny access immediately.
if (in_array(FALSE, $grants, TRUE)) {
return FALSE;
}
// At least one grant has the explicit opinion to allow access.
if (in_array(TRUE, $grants, TRUE)) {
return TRUE;
}
// All grants are NULL and have no opinion - deny access in that case.
return FALSE;
$access_controller = \Drupal::entityManager()->getAccessController($this->getParent()->entityType());
return $access_controller->fieldAccess($operation, $this->getFieldDefinition(), $account, $this);
}
/**
* Contains the default access logic of this field.
*
* See \Drupal\Core\TypedData\AccessibleInterface::access() for the parameter
* doucmentation. This method can be overriden by field sub classes to provide
* a different default access logic. That allows them to inherit the complete
* access() method which contains the access hook invocation logic.
*
* @return bool
* TRUE if access to this field is allowed per default, FALSE otherwise.
* {@inheritdoc}
*/
public function defaultAccess($operation = 'view', AccountInterface $account = NULL) {
// Grant access per default.
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Entity\Field;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AccessibleInterface;
use Drupal\Core\TypedData\ListInterface;
......@@ -59,6 +60,17 @@ public function getLangcode();
*/
public function getFieldDefinition();
/**
* Contains the default access logic of this field.
*
* See \Drupal\Core\Entity\EntityAccessControllerInterface::fieldAccess() for
* the parameter documentation.
*
* @return bool
* TRUE if access to this field is allowed per default, FALSE otherwise.
*/
public function defaultAccess($operation = 'view', AccountInterface $account = NULL);
/**
* Filters out empty field items and re-numbers the item deltas.
*/
......
......@@ -71,8 +71,7 @@ public function access(Route $route, Request $request) {
* {@inheritdoc}
*/
public function accessEditEntityField(EntityInterface $entity, $field_name) {
$entity_type = $entity->entityType();
return $entity->access('update') && ($field = $this->fieldInfo->getField($entity_type, $field_name)) && field_access('edit', $field, $entity_type, $entity);
return $entity->access('update') && $entity->get($field_name)->access('edit');
}
/**
......
......@@ -12,6 +12,8 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Controller\ControllerInterface;
use Drupal\Core\Entity\EntityManager;
/**
* Defines route controller for entity reference.
......@@ -25,14 +27,24 @@ class EntityReferenceController implements ContainerInjectionInterface {
*/
protected $entityReferenceAutocomplete;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Constructs a EntityReferenceController object.
*
* @param \Drupal\entity_reference\EntityReferenceAutocomplete $entity_reference_autcompletion
* The autocompletion helper for entity references
* The autocompletion helper for entity references.
* @param \Drupal\Core\Entity\EntityManager êntity_manager
* The entity manager.
*/
public function __construct(EntityReferenceAutocomplete $entity_reference_autcompletion) {
public function __construct(EntityReferenceAutocomplete $entity_reference_autcompletion, EntityManager $entity_manager) {
$this->entityReferenceAutocomplete = $entity_reference_autcompletion;
$this->entityManager = $entity_manager;
}
/**
......@@ -40,7 +52,8 @@ public function __construct(EntityReferenceAutocomplete $entity_reference_autcom
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_reference.autocomplete')
$container->get('entity_reference.autocomplete'),
$container->get('entity.manager')
);
}
......@@ -77,7 +90,8 @@ public function handleAutocomplete(Request $request, $type, $field_name, $entity
throw new AccessDeniedHttpException();
}
if ($field['type'] != 'entity_reference' || !field_access('edit', $field, $entity_type)) {
$access_controller = $this->entityManager->getAccessController($entity_type);
if ($field['type'] != 'entity_reference' || !$access_controller->fieldAccess('edit', $instance)) {
throw new AccessDeniedHttpException();
}
......
......@@ -572,33 +572,6 @@ function hook_field_purge_instance($instance) {
* @} End of "addtogroup field_crud".
*/
/**
* Determine whether the user has access to a given field.
*
* This hook is invoked from field_access() to let modules block access to
* operations on fields. If no module returns FALSE, the operation is allowed.
*
* @param $op
* The operation to be performed. Possible values: 'edit', 'view'.
* @param \Drupal\field\FieldInterface $field
* The field on which the operation is to be performed.
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* (optional) The entity for the operation.
* @param $account
* (optional) The account to check; if not given use currently logged in user.
*
* @return
* TRUE if the operation is allowed, and FALSE if the operation is denied.
*/
function hook_field_access($op, \Drupal\field\FieldInterface $field, $entity_type, $entity, $account) {
if ($field['field_name'] == 'field_of_interest' && $op == 'edit') {
return $account->hasPermission('edit field of interest');
}
return TRUE;
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -829,3 +829,32 @@ function field_get_items(EntityInterface $entity, $field_name, $langcode = NULL)
function field_get_default_value(EntityInterface $entity, $field, $instance, $langcode = NULL) {
return $instance->getFieldDefaultValue($entity);
}
/**
* Determines whether the user has access to a given field.
*
* @param string $op
* The operation to be performed. Possible values:
* - edit
* - view
* @param \Drupal\field\FieldInterface $field
* The field on which the operation is to be performed.
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* (optional) The entity for the operation.
* @param $account
* (optional) The account to check, if not given use currently logged in user.
*
* @return
* TRUE if the operation is allowed; FALSE if the operation is denied.
*
* @deprecated as of Drupal 8.0. Use
* Drupal\Core\Entity\EntityAccessControllerInterface::fieldAccess()
*/
function field_access($op, FieldInterface $field, $entity_type, $entity = NULL, $account = NULL) {
$access_controller = \Drupal::entityManager()->getAccessController($entity_type);
$items = $entity ? $entity->get($field->id()) : NULL;
return $access_controller->fieldAccess($op, $field, $account, $items);
}
......@@ -592,42 +592,6 @@ function field_view_field(EntityInterface $entity, $field_name, $display_options
return $output;
}
/**
* Determines whether the user has access to a given field.
*
* @param string $op
* The operation to be performed. Possible values:
* - edit
* - view
* @param \Drupal\field\FieldInterface $field
* The field on which the operation is to be performed.
* @param $entity_type
* The type of $entity; for example, 'node' or 'user'.
* @param $entity
* (optional) The entity for the operation.
* @param $account
* (optional) The account to check, if not given use currently logged in user.
*
* @return
* TRUE if the operation is allowed; FALSE if the operation is denied.
*/
function field_access($op, FieldInterface $field, $entity_type, $entity = NULL, $account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
foreach (\Drupal::moduleHandler()->getImplementations('field_access') as $module) {
$function = $module . '_field_access';
$access = $function($op, $field, $entity_type, $entity, $account);
if ($access === FALSE) {
return FALSE;
}
}
return TRUE;
}
/**
* Extracts the bundle name from a bundle object.
*
......
......@@ -718,4 +718,11 @@ public function __wakeup() {
$this->__construct($values);
}
/**
* {@inheritdoc}
*/
public function isFieldConfigurable() {
return TRUE;
}
}
......@@ -607,6 +607,13 @@ public function allowBundleRename() {
$this->bundle_rename_allowed = TRUE;
}
/**
* {@inheritdoc}
*/
public function isFieldConfigurable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
......
......@@ -84,7 +84,7 @@ public function view(FieldItemListInterface $items) {
$info = array(
'#theme' => 'field',
'#title' => $this->fieldDefinition->getFieldLabel(),
'#access' => $this->checkFieldAccess('view', $entity),
'#access' => $items->access('view'),
'#label_display' => $this->label,
'#view_mode' => $this->viewMode,
'#language' => $items->getLangcode(),
......@@ -136,22 +136,6 @@ public function settingsSummary() {
*/
public function prepareView(array $entities_items) { }
/**
* Returns whether the currently logged in user has access to the field.