EntityAccessController.php 11.1 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Entity\EntityAccessController.
6 7 8 9
 */

namespace Drupal\Core\Entity;

10
use Drupal\Core\Field\FieldItemListInterface;
11
use Drupal\Core\Extension\ModuleHandlerInterface;
12
use Drupal\Core\Field\FieldDefinitionInterface;
13
use Drupal\Core\Language\Language;
14
use Drupal\Core\Session\AccountInterface;
15 16

/**
17
 * Defines a default implementation for entity access controllers.
18
 */
19
class EntityAccessController implements EntityAccessControllerInterface {
20

21 22 23 24 25 26 27
  /**
   * Stores calculcated access check results.
   *
   * @var array
   */
  protected $accessCache = array();

28 29 30 31 32 33 34
  /**
   * The entity type of the access controller instance.
   *
   * @var string
   */
  protected $entityType;

35 36 37 38 39 40 41
  /**
   * The entity info array.
   *
   * @var array
   */
  protected $entityInfo;

42 43 44 45 46 47 48
  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

49 50 51 52 53
  /**
   * Constructs an access controller instance.
   *
   * @param string $entity_type
   *   The entity type of the access controller instance.
54 55
   * @param array $entity_info
   *   An array of entity info for the entity type.
56
   */
57
  public function __construct($entity_type, array $entity_info) {
58
    $this->entityType = $entity_type;
59
    $this->entityInfo = $entity_info;
60 61
  }

62
  /**
63
   * {@inheritdoc}
64
   */
65
  public function access(EntityInterface $entity, $operation, $langcode = Language::LANGCODE_DEFAULT, AccountInterface $account = NULL) {
66
    $account = $this->prepareUser($account);
67

68
    if (($access = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
69
      // Cache hit, no work necessary.
70 71 72
      return $access;
    }

73 74 75 76 77
    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
    // take precedence over overridden implementations of
    // EntityAccessController::checkAccess(). Entities that have checks that
    // need to be done before the hook is invoked should do so by overriding
    // this method.
78

79 80 81
    // We grant access to the entity if both of these conditions are met:
    // - No modules say to deny access.
    // - At least one module says to grant access.
82 83 84 85
    $access = array_merge(
      $this->moduleHandler->invokeAll('entity_access', array($entity, $operation, $account, $langcode)),
      $this->moduleHandler->invokeAll($entity->entityType() . '_access', array($entity, $operation, $account, $langcode))
    );
86

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    if (($return = $this->processAccessHookResults($access)) === NULL) {
      // No module had an opinion about the access, so let's the access
      // controller check create access.
      $return = (bool) $this->checkAccess($entity, $operation, $langcode, $account);
    }
    return $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
  }

  /**
   * We grant access to the entity if both of these conditions are met:
   * - No modules say to deny access.
   * - At least one module says to grant access.
   *
   * @param array $access
   *   An array of access results of the fired access hook.
   *
103
   * @return bool|null
104 105 106 107
   *   Returns FALSE if access should be denied, TRUE if access should be
   *   granted and NULL if no module denied access.
   */
  protected function processAccessHookResults(array $access) {
108
    if (in_array(FALSE, $access, TRUE)) {
109
      return FALSE;
110 111
    }
    elseif (in_array(TRUE, $access, TRUE)) {
112
      return TRUE;
113 114
    }
    else {
115
      return;
116
    }
117 118 119
  }

  /**
120 121 122 123
   * Performs access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * do their own custom access checking.
124 125 126 127
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to check 'create' access.
   * @param string $operation
128
   *   The entity operation. Usually one of 'view', 'update', 'create' or
129 130
   *   'delete'.
   * @param string $langcode
131
   *   The language code for which to check access.
132
   * @param \Drupal\Core\Session\AccountInterface $account
133
   *   The user for which to check access.
134 135 136 137 138
   *
   * @return bool|null
   *   TRUE if access was granted, FALSE if access was denied and NULL if access
   *   could not be determined.
   */
139
  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
140 141 142 143 144 145
    if (!empty($this->entityInfo['admin_permission'])) {
      return $account->hasPermission($this->entityInfo['admin_permission']);
    }
    else {
      return NULL;
    }
146 147 148 149 150
  }

  /**
   * Tries to retrieve a previously cached access value from the static cache.
   *
151 152 153
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
154
   * @param string $operation
155
   *   The entity operation. Usually one of 'view', 'update', 'create' or
156 157
   *   'delete'.
   * @param string $langcode
158
   *   The language code for which to check access.
159
   * @param \Drupal\Core\Session\AccountInterface $account
160
   *   The user for which to check access.
161 162 163 164 165 166
   *
   * @return bool|null
   *   TRUE if access was granted, FALSE if access was denied and NULL if there
   *   is no record for the given user, operation, langcode and entity in the
   *   cache.
   */
167
  protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
168
    // Return from cache if a value has been set for it previously.
169 170
    if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
      return $this->accessCache[$account->id()][$cid][$langcode][$operation];
171 172 173 174 175 176
    }
  }

  /**
   * Statically caches whether the given user has access.
   *
177 178
   * @param bool $access
   *   TRUE if the user has access, FALSE otherwise.
179 180 181
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
182
   * @param string $operation
183
   *   The entity operation. Usually one of 'view', 'update', 'create' or
184 185
   *   'delete'.
   * @param string $langcode
186
   *   The language code for which to check access.
187
   * @param \Drupal\Core\Session\AccountInterface $account
188
   *   The user for which to check access.
189 190 191 192
   *
   * @return bool
   *   TRUE if access was granted, FALSE otherwise.
   */
193
  protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {
194
    // Save the given value in the static cache and directly return it.
195
    return $this->accessCache[$account->id()][$cid][$langcode][$operation] = (bool) $access;
196 197 198
  }

  /**
199
   * {@inheritdoc}
200 201 202
   */
  public function resetCache() {
    $this->accessCache = array();
203 204
  }

205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
  /**
   * {@inheritdoc}
   */
  public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = array()) {
    $account = $this->prepareUser($account);
    $context += array(
      'langcode' => Language::LANGCODE_DEFAULT,
    );

    $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
    if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {
      // Cache hit, no work necessary.
      return $access;
    }

220 221 222 223 224
    // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
    // Hook results take precedence over overridden implementations of
    // EntityAccessController::checkAccess(). Entities that have checks that
    // need to be done before the hook is invoked should do so by overriding
    // this method.
225 226 227 228

    // We grant access to the entity if both of these conditions are met:
    // - No modules say to deny access.
    // - At least one module says to grant access.
229 230 231 232
    $access = array_merge(
      $this->moduleHandler->invokeAll('entity_create_access', array($account, $context['langcode'])),
      $this->moduleHandler->invokeAll($this->entityType . '_create_access', array($account, $context['langcode']))
    );
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

    if (($return = $this->processAccessHookResults($access)) === NULL) {
      // No module had an opinion about the access, so let's the access
      // controller check create access.
      $return = (bool) $this->checkCreateAccess($account, $context, $entity_bundle);
    }
    return $this->setCache($return, $cid, 'create', $context['langcode'], $account);
  }

  /**
   * Performs create access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * do their own custom access checking.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for which to check access.
   * @param array $context
   *   An array of key-value pairs to pass additional context when needed.
   * @param string|null $entity_bundle
   *   (optional) The bundle of the entity. Required if the entity supports
   *   bundles, defaults to NULL otherwise.
   *
   * @return bool|null
   *   TRUE if access was granted, FALSE if access was denied and NULL if access
   *   could not be determined.
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
261 262 263 264 265 266
    if (!empty($this->entityInfo['admin_permission'])) {
      return $account->hasPermission($this->entityInfo['admin_permission']);
    }
    else {
      return NULL;
    }
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
  }

  /**
   * Loads the current account object, if it does not exist yet.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account interface instance.
   *
   * @return \Drupal\Core\Session\AccountInterface
   *   Returns the current account object.
   */
  protected function prepareUser(AccountInterface $account = NULL) {
    if (!$account) {
      $account = $GLOBALS['user'];
    }
    return $account;
  }

285 286 287 288 289 290 291 292
  /**
   * {@inheritdoc}
   */
  public function setModuleHandler(ModuleHandlerInterface $module_handler) {
    $this->moduleHandler = $module_handler;
    return $this;
  }

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
  /**
   * {@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;
  }

331
}