Commit 44eda14f authored by Dries's avatar Dries

Issue #1862750 by fubhy, Berdir, dawehner: Implement entity access API for nodes.

parent 261a3752
......@@ -10,39 +10,173 @@
use Drupal\user\Plugin\Core\Entity\User;
/**
* Defines a base class for entity access controllers.
*
* Defaults to FALSE (access denied) for 'view', 'create', 'update' and 'delete'
* access checks.
* Defines a default implementation for entity access controllers.
*/
class EntityAccessController implements EntityAccessControllerInterface {
/**
* Stores calculcated access check results.
*
* @var array
*/
protected $accessCache = array();
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::viewAccess().
*/
public function viewAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return FALSE;
if (($access = $this->getCache($entity, 'view', $langcode, $account)) !== NULL) {
return $access;
}
$access = (bool) $this->access($entity, 'view', $langcode, $account);
return $this->setCache($access, $entity, 'view', $langcode, $account);
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::createAccess().
*/
public function createAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return FALSE;
if (($access = $this->getCache($entity, 'create', $langcode, $account)) !== NULL) {
return $access;
}
$access = (bool) $this->access($entity, 'create', $langcode, $account);
return $this->setCache($access, $entity, 'create', $langcode, $account);
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::updateAccess().
*/
public function updateAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return FALSE;
if (($access = $this->getCache($entity, 'update', $langcode, $account)) !== NULL) {
return $access;
}
$access = (bool) $this->access($entity, 'update', $langcode, $account);
return $this->setCache($access, $entity, 'update', $langcode, $account);
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::deleteAccess().
*/
public function deleteAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return FALSE;
if (($access = $this->getCache($entity, 'delete', $langcode, $account)) !== NULL) {
return $access;
}
$access = (bool) $this->access($entity, 'delete', $langcode, $account);
return $this->setCache($access, $entity, 'delete', $langcode, $account);
}
/**
* Performs default, shared access checks.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check 'create' access.
* @param string $operation
* The entity operation. Usually one of 'view', 'edit', 'create' or
* 'delete'.
* @param string $langcode
* (optional) The language code for which to check access. Defaults to
* LANGUAGE_DEFAULT.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*
* @return bool|null
* TRUE if access was granted, FALSE if access was denied and NULL if access
* could not be determined.
*/
protected function access(EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
// @todo Remove this once we can rely on $account.
if (!$account) {
$account = user_load($GLOBALS['user']->uid);
}
// 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.
$access = module_invoke_all($entity->entityType() . '_access', $entity, $operation, $account, $langcode);
if (in_array(FALSE, $access, TRUE)) {
return FALSE;
}
elseif (in_array(TRUE, $access, TRUE)) {
return TRUE;
}
}
/**
* Tries to retrieve a previously cached access value from the static cache.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check 'create' access.
* @param string $operation
* The entity operation. Usually one of 'view', 'edit', 'create' or
* 'delete'.
* @param string $langcode
* (optional) The language code for which to check access. Defaults to
* LANGUAGE_DEFAULT.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*
* @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.
*/
protected function getCache(EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
// @todo Remove this once we can rely on $account.
if (!$account) {
$account = user_load($GLOBALS['user']->uid);
}
$uid = $account ? $account->id() : 0;
$uuid = $entity->uuid();
// Return from cache if a value has been set for it previously.
if (isset($this->accessCache[$uid][$uuid][$langcode][$operation])) {
return $this->accessCache[$uid][$uuid][$langcode][$operation];
}
}
/**
* Statically caches whether the given user has access.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check 'create' access.
* @param string $operation
* The entity operation. Usually one of 'view', 'edit', 'create' or
* 'delete'.
* @param string $langcode
* (optional) The language code for which to check access. Defaults to
* LANGUAGE_DEFAULT.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*
* @return bool
* TRUE if access was granted, FALSE otherwise.
*/
protected function setCache($access, EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
// @todo Remove this once we can rely on $account.
if (!$account) {
$account = user_load($GLOBALS['user']->uid);
}
$uid = $account ? $account->id() : 0;
$uuid = $entity->uuid();
// Save the given value in the static cache and directly return it.
return $this->accessCache[$uid][$uuid][$langcode][$operation] = (bool) $access;
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::clearCache().
*/
public function resetCache() {
$this->accessCache = array();
}
}
......@@ -82,4 +82,9 @@ public function updateAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAU
* TRUE if access was granted, FALSE otherwise.
*/
public function deleteAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL);
/**
* Clears all cached access checks.
*/
public function resetCache();
}
......@@ -9,12 +9,12 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\Plugin\Core\Entity\User;
use Drupal\Core\Entity\EntityAccessControllerInterface;
use Drupal\Core\Entity\EntityAccessController;
/**
* Defines the access controller for the custom block entity type.
*/
class CustomBlockAccessController implements EntityAccessControllerInterface {
class CustomBlockAccessController extends EntityAccessController {
/**
* Implements EntityAccessControllerInterface::viewAccess().
......
<?php
/**
* @file
* Contains \Drupal\node\NodeAccessController.
*/
namespace Drupal\node;
use Drupal\user\Plugin\Core\Entity\User;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityNG;
/**
* Defines the access controller for the node entity type.
*/
class NodeAccessController extends EntityAccessController {
/**
* Overrides \Drupal\Core\Entity\EntityAccessController::viewAccess().
*/
public function viewAccess(EntityInterface $node, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
if (($cached = $this->getCache($node, 'view', $langcode, $account)) !== NULL ) {
return $cached;
}
if (($access = $this->access($node, 'view', $langcode, $account)) !== NULL) {
return $this->setCache((bool) $access, $node, 'view', $langcode, $account);
};
// If no modules implement hook_node_grants(), the default behavior is to
// allow all users to view published nodes, so reflect that here.
$status = $node instanceof EntityNG ? $node->getTranslation($langcode, FALSE)->status->value : $node->status;
return $this->setCache($status, $node, 'view', $langcode, $account);
}
/**
* Overrides \Drupal\Core\Entity\EntityAccessController::access().
*/
protected function access(EntityInterface $node, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
if (user_access('bypass node access', $account)) {
return TRUE;
}
if (!user_access('access content', $account)) {
return FALSE;
}
if (($access = parent::access($node, $operation, $langcode, $account)) !== NULL) {
return (bool) $access;
};
// Fetch information from the node object if possible.
$status = isset($node->status) ? $node->status : NULL;
$uid = isset($node->uid) ? $node->uid : NULL;
// If it is a proper EntityNG object, use the proper methods.
if ($node instanceof EntityNG) {
$status = $node->getTranslation($langcode, FALSE)->status->value;
$uid = $node->getTranslation($langcode, FALSE)->uid->value;
}
// Check if authors can view their own unpublished nodes.
if ($operation == 'view' && !$status && user_access('view own unpublished content', $account)) {
// @todo Remove this once we can rely on $account.
if (!$account) {
$account = user_load($GLOBALS['user']->uid);
}
if ($account->id() != 0 && $account->id() == $uid) {
return TRUE;
}
}
// If no module specified either allow or deny, we fall back to the
// node_access table.
if (($grants = $this->accessGrants($node, $operation, $langcode, $account)) !== NULL) {
return (bool) $grants;
}
}
/**
* Determines access to nodes based on node grants.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check 'create' access.
* @param string $operation
* The entity operation. Usually one of 'view', 'edit', 'create' or
* 'delete'.
* @param string $langcode
* (optional) The language code for which to check access. Defaults to
* LANGUAGE_DEFAULT.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*
* @return bool|null
* TRUE if access was granted, FALSE if access was denied or NULL if no
* module implements hook_node_grants(), the node does not (yet) have an id
* or none of the implementing modules explicitly granted or denied access.
*/
protected function accessGrants(EntityInterface $node, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
// If no module implements the hook or the node does not have an id there is
// no point in querying the database for access grants.
if (!module_implements('node_grants') || !$node->id()) {
return;
}
// Check the database for potential access grants.
$query = db_select('node_access');
$query->addExpression('1');
$query->condition('grant_' . $operation, 1, '>=');
$nids = db_or()->condition('nid', $node->id());
$status = $node instanceof EntityNG ? $node->status : $node->get('status', $langcode)->value;
if ($status) {
$nids->condition('nid', 0);
}
$query->condition($nids);
$query->range(0, 1);
$grants = db_or();
foreach (node_access_grants($operation, $account) as $realm => $gids) {
foreach ($gids as $gid) {
$grants->condition(db_and()
->condition('gid', $gid)
->condition('realm', $realm));
}
}
if (count($grants) > 0) {
$query->condition($grants);
}
return $query->execute()->fetchField();
}
}
......@@ -22,6 +22,7 @@
* module = "node",
* controller_class = "Drupal\node\NodeStorageController",
* render_controller_class = "Drupal\node\NodeRenderController",
* access_controller_class = "Drupal\node\NodeAccessController",
* form_controller_class = {
* "default" = "Drupal\node\NodeFormController"
* },
......
......@@ -79,7 +79,7 @@ function testNodeAccess() {
$this->assertNodeAccess($expected_node_access, $node, $web_user, 'hr');
// Reset the node access cache and turn on our test node_access() code.
drupal_static_reset('node_access');
entity_access_controller('node')->resetCache();
state()->set('node_access_test_secret_catalan', 1);
// Tests that Hungarian is still accessible.
......
......@@ -19,6 +19,7 @@
use Drupal\Core\Template\Attribute;
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
use Drupal\file\Plugin\Core\Entity\File;
use Drupal\user\Plugin\Core\Entity\User;
/**
* Denotes that the node is not published.
......@@ -56,7 +57,7 @@
* Modules should return this value from hook_node_access() to allow access to a
* node.
*/
const NODE_ACCESS_ALLOW = 'allow';
const NODE_ACCESS_ALLOW = TRUE;
/**
* Denotes that access is denied for a node.
......@@ -64,7 +65,7 @@
* Modules should return this value from hook_node_access() to deny access to a
* node.
*/
const NODE_ACCESS_DENY = 'deny';
const NODE_ACCESS_DENY = FALSE;
/**
* Denotes that access is unaffected for a node.
......@@ -2513,126 +2514,31 @@ function node_form_system_themes_admin_form_submit($form, &$form_state) {
* http://drupal.org/node/1658846
*/
function node_access($op, $node, $account = NULL, $langcode = NULL) {
$rights = &drupal_static(__FUNCTION__, array());
if (!$node || !in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
// If there was no node to check against, or the $op was not one of the
// supported ones, we return access denied.
return FALSE;
}
// If no user object is supplied, the access check is for the current user.
if (empty($account)) {
$account = $GLOBALS['user'];
if (!$node instanceof EntityInterface) {
$type = is_object($node) ? $node->type : $node;
$node = entity_create('node', array('type' => $type));
}
// $node may be a node object, a node type object, or a node type string.
// Use the node ID as the primary cache ID, or the node type name otherwise.
$cid = is_object($node) ? (isset($node->nid) ? $node->nid : $node->type) : $node;
// If no language code was provided, default to the node's langcode or
// to an empty langcode if a node type was requested. The latter is purely
// for caching purposes.
// If no language code was provided, default to the node's langcode.
if (empty($langcode)) {
$langcode = (is_object($node) && isset($node->nid)) ? $node->langcode : '';
}
// Fetch information from the node object if possible.
$node_status = NULL;
$node_uid = NULL;
if (is_object($node)) {
$node_status = (isset($node->status)) ? $node->status : $node_status;
$node_uid = (isset($node->uid)) ? $node->uid : $node_uid;
// If it is a proper EntityNG object, use the proper methods.
if ($node instanceof \Drupal\Core\Entity\EntityNG){
$node_status = $node->get('status', $langcode)->value;
$node_uid = $node->get('uid', $langcode)->value ;
}
}
// If we've already checked access for this node, user and op, return from
// cache.
if (isset($rights[$account->uid][$cid][$langcode][$op])) {
return $rights[$account->uid][$cid][$langcode][$op];
}
if (user_access('bypass node access', $account)) {
$rights[$account->uid][$cid][$langcode][$op] = TRUE;
return TRUE;
}
if (!user_access('access content', $account)) {
$rights[$account->uid][$cid][$langcode][$op] = FALSE;
return FALSE;
}
// We grant access to the node if both of the following conditions are met:
// - No modules say to deny access.
// - At least one module says to grant access.
// If no module specified either allow or deny, we fall back to the
// node_access table.
$access = module_invoke_all('node_access', $node, $op, $account, $langcode);
if (in_array(NODE_ACCESS_DENY, $access, TRUE)) {
$rights[$account->uid][$cid][$langcode][$op] = FALSE;
return FALSE;
}
elseif (in_array(NODE_ACCESS_ALLOW, $access, TRUE)) {
$rights[$account->uid][$cid][$langcode][$op] = TRUE;
return TRUE;
}
// Check if authors can view their own unpublished nodes.
if ($op == 'view' && empty($node_status) && user_access('view own unpublished content', $account) && $account->uid === $node_uid && $account->uid != 0) {
$rights[$account->uid][$cid][$langcode][$op] = TRUE;
return TRUE;
$langcode = $node->langcode;
}
// If the module did not override the access rights, use those set in the
// node_access table.
if ($op != 'create' && !empty($node->nid)) {
if (module_implements('node_grants')) {
$query = db_select('node_access');
$query->addExpression('1');
$query->condition('grant_' . $op, 1, '>=');
$nids = db_or()->condition('nid', $node->nid);
if ($node->status) {
$nids->condition('nid', 0);
}
$query->condition($nids);
$query->range(0, 1);
$grants = db_or();
foreach (node_access_grants($op, $account) as $realm => $gids) {
foreach ($gids as $gid) {
$grants->condition(db_and()
->condition('gid', $gid)
->condition('realm', $realm)
);
}
}
if (count($grants) > 0) {
$query->condition($grants);
}
$result = (bool) $query
->execute()
->fetchField();
$rights[$account->uid][$cid][$langcode][$op] = $result;
return $result;
}
elseif (is_object($node) && $op == 'view' && !empty($node_status)) {
// If no modules implement hook_node_grants(), the default behavior is to
// allow all users to view published nodes, so reflect that here.
$rights[$account->uid][$cid][$langcode][$op] = TRUE;
return TRUE;
}
// Make sure that if an account is passed, that it is a fully loaded user
// object.
if ($account && !($account instanceof User)) {
$account = user_load($account->uid);
}
return FALSE;
$method = $op . 'Access';
return entity_access_controller('node')->$method($node, $langcode, $account);
}
/**
* Implements hook_node_access().
*/
function node_node_access($node, $op, $account) {
$type = is_string($node) ? $node : $node->type;
$type = $node->bundle();
$configured_types = node_permissions_get_configured_types();
if (isset($configured_types[$type])) {
......
......@@ -8,13 +8,13 @@
namespace Drupal\entity_test;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControllerInterface;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\user\Plugin\Core\Entity\User;
/**
* Defines the access controller for the test entity type.
*/
class EntityTestAccessController implements EntityAccessControllerInterface {
class EntityTestAccessController extends EntityAccessController {
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::viewAccess().
......
......@@ -8,13 +8,13 @@
namespace Drupal\user;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControllerInterface;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\user\Plugin\Core\Entity\User;
/**
* Defines the access controller for the user entity type.
*/
class UserAccessController implements EntityAccessControllerInterface {
class UserAccessController extends EntityAccessController {
/**
* Implements EntityAccessControllerInterface::viewAccess().
......
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