EntityAccessControlHandler.php 12.1 KB
Newer Older
1
2
3
4
<?php

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

namespace Drupal\Core\Entity;

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

/**
16
 * Defines a default implementation for entity access control handler.
17
 */
18
class EntityAccessControlHandler extends EntityControllerBase implements EntityAccessControlHandlerInterface {
19

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

27
  /**
28
   * The entity type ID of the access control handler instance.
29
30
31
   *
   * @var string
   */
32
  protected $entityTypeId;
33

34
  /**
35
   * Information about the entity type.
36
   *
37
   * @var \Drupal\Core\Entity\EntityTypeInterface
38
   */
39
  protected $entityType;
40

41
  /**
42
   * Constructs an access control handler instance.
43
   *
44
45
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
46
   */
47
48
49
  public function __construct(EntityTypeInterface $entity_type) {
    $this->entityTypeId = $entity_type->id();
    $this->entityType = $entity_type;
50
51
  }

52
  /**
53
   * {@inheritdoc}
54
   */
55
  public function access(EntityInterface $entity, $operation, $langcode = LanguageInterface::LANGCODE_DEFAULT, AccountInterface $account = NULL) {
56
    $account = $this->prepareUser($account);
57

58
    if (($access = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
59
      // Cache hit, no work necessary.
60
61
62
      return $access;
    }

63
64
    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
    // take precedence over overridden implementations of
65
    // EntityAccessControlHandler::checkAccess(). Entities that have checks that
66
67
    // need to be done before the hook is invoked should do so by overriding
    // this method.
68

69
70
71
    // 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.
72
    $access = array_merge(
73
      $this->moduleHandler()->invokeAll('entity_access', array($entity, $operation, $account, $langcode)),
74
      $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', array($entity, $operation, $account, $langcode))
75
    );
76

77
78
    if (($return = $this->processAccessHookResults($access)) === NULL) {
      // No module had an opinion about the access, so let's the access
79
      // handler check create access.
80
81
82
83
84
85
86
87
88
89
90
91
92
      $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.
   *
93
   * @return bool|null
94
95
96
97
   *   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) {
98
    if (in_array(FALSE, $access, TRUE)) {
99
      return FALSE;
100
101
    }
    elseif (in_array(TRUE, $access, TRUE)) {
102
      return TRUE;
103
104
    }
    else {
105
      return;
106
    }
107
108
109
  }

  /**
110
111
112
113
   * Performs access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * do their own custom access checking.
114
115
116
117
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to check 'create' access.
   * @param string $operation
118
   *   The entity operation. Usually one of 'view', 'update', 'create' or
119
120
   *   'delete'.
   * @param string $langcode
121
   *   The language code for which to check access.
122
   * @param \Drupal\Core\Session\AccountInterface $account
123
   *   The user for which to check access.
124
125
126
127
128
   *
   * @return bool|null
   *   TRUE if access was granted, FALSE if access was denied and NULL if access
   *   could not be determined.
   */
129
  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
130
131
132
    if ($operation == 'delete' && $entity->isNew()) {
      return FALSE;
    }
133
    if ($admin_permission = $this->entityType->getAdminPermission()) {
134
      return $account->hasPermission($admin_permission);
135
136
137
138
    }
    else {
      return NULL;
    }
139
140
141
142
143
  }

  /**
   * Tries to retrieve a previously cached access value from the static cache.
   *
144
145
146
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
147
   * @param string $operation
148
   *   The entity operation. Usually one of 'view', 'update', 'create' or
149
150
   *   'delete'.
   * @param string $langcode
151
   *   The language code for which to check access.
152
   * @param \Drupal\Core\Session\AccountInterface $account
153
   *   The user for which to check access.
154
155
156
157
158
159
   *
   * @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.
   */
160
  protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
161
    // Return from cache if a value has been set for it previously.
162
163
    if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
      return $this->accessCache[$account->id()][$cid][$langcode][$operation];
164
165
166
167
168
169
    }
  }

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

  /**
192
   * {@inheritdoc}
193
194
195
   */
  public function resetCache() {
    $this->accessCache = array();
196
197
  }

198
199
200
201
202
203
  /**
   * {@inheritdoc}
   */
  public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = array()) {
    $account = $this->prepareUser($account);
    $context += array(
204
      'langcode' => LanguageInterface::LANGCODE_DEFAULT,
205
206
207
208
209
210
211
212
    );

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

213
214
    // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
    // Hook results take precedence over overridden implementations of
215
    // EntityAccessControlHandler::checkAccess(). Entities that have checks that
216
217
    // need to be done before the hook is invoked should do so by overriding
    // this method.
218
219
220
221

    // 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.
222
    $access = array_merge(
223
224
      $this->moduleHandler()->invokeAll('entity_create_access', array($account, $context, $entity_bundle)),
      $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', array($account, $context, $entity_bundle))
225
    );
226
227
228

    if (($return = $this->processAccessHookResults($access)) === NULL) {
      // No module had an opinion about the access, so let's the access
229
      // handler check create access.
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
      $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) {
254
    if ($admin_permission = $this->entityType->getAdminPermission()) {
255
      return $account->hasPermission($admin_permission);
256
257
258
259
    }
    else {
      return NULL;
    }
260
261
262
263
264
265
266
267
268
269
270
271
272
  }

  /**
   * 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) {
273
      $account = \Drupal::currentUser();
274
275
276
277
    }
    return $account;
  }

278
279
280
281
282
283
284
285
286
  /**
   * {@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;

287
288
289
290
291
292
    // Get the default access restriction as specified by the access controller.
    $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);

    // Combine default access, denying access wins.
    $default = $default && $entity_default;

293
294
295
    // 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);
296
    $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
297
    foreach ($hook_implementations as $module) {
298
      $grants = array_merge($grants, array($module => $this->moduleHandler()->invoke($module, 'entity_field_access', array($operation, $field_definition, $account, $items))));
299
300
301
302
303
304
305
306
307
    }

    // Also allow modules to alter the returned grants/denies.
    $context = array(
      'operation' => $operation,
      'field_definition' => $field_definition,
      'items' => $items,
      'account' => $account,
    );
308
    $this->moduleHandler()->alter('entity_field_access', $grants, $context);
309
310
311
312
313
314
315
316
317
318
319
320
321

    // 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;
  }

322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
  /**
   * Default field access as determined by this access controller.
   *
   * @param string $operation
   *   The operation access should be checked for.
   *   Usually one of "view" or "edit".
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The field definition.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user session for which to check access.
   * @param \Drupal\Core\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.
   *
   * @return bool
   *   TRUE if access is allowed, FALSE otherwise.
   */
  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
    return TRUE;
  }

344
}