diff --git a/core/lib/Drupal/Core/Action/ActionInterface.php b/core/lib/Drupal/Core/Action/ActionInterface.php index fe04acb..cbf826f 100644 --- a/core/lib/Drupal/Core/Action/ActionInterface.php +++ b/core/lib/Drupal/Core/Action/ActionInterface.php @@ -9,6 +9,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Executable\ExecutableInterface; +use Drupal\Core\Session\AccountInterface; /** * Provides an interface for an Action plugin. @@ -44,4 +45,24 @@ */ public function executeMultiple(array $objects); + /** + * Checks object access. + * + * @param mixed $object + * The object to execute the action on. + * @param \Drupal\Core\Session\AccountInterface $account + * (optional) The user for which to check access, or NULL to check access + * for the current user. Defaults to NULL. + * @param bool $return_as_object + * (optional) Defaults to FALSE. + * + * @return bool|\Drupal\Core\Access\AccessResultInterface + * The access result. Returns a boolean if $return_as_object is FALSE (this + * is the default) and otherwise an AccessResultInterface object. + * When a boolean is returned, the result of AccessInterface::isAllowed() is + * returned, i.e. TRUE means access is explicitly allowed, FALSE means + * access is either explicitly forbidden or "no opinion". + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE); + } diff --git a/core/modules/action/src/Plugin/Action/EmailAction.php b/core/modules/action/src/Plugin/Action/EmailAction.php index 5bdc609..d93152d 100644 --- a/core/modules/action/src/Plugin/Action/EmailAction.php +++ b/core/modules/action/src/Plugin/Action/EmailAction.php @@ -7,11 +7,13 @@ namespace Drupal\action\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Mail\MailManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Utility\Token; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -184,4 +186,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['message'] = $form_state->getValue('message'); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/action/src/Plugin/Action/GotoAction.php b/core/modules/action/src/Plugin/Action/GotoAction.php index 833f304..c925c5c 100644 --- a/core/modules/action/src/Plugin/Action/GotoAction.php +++ b/core/modules/action/src/Plugin/Action/GotoAction.php @@ -7,10 +7,12 @@ namespace Drupal\action\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -113,4 +115,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['url'] = $form_state->getValue('url'); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/action/src/Plugin/Action/MessageAction.php b/core/modules/action/src/Plugin/Action/MessageAction.php index 7996514..c87b606 100644 --- a/core/modules/action/src/Plugin/Action/MessageAction.php +++ b/core/modules/action/src/Plugin/Action/MessageAction.php @@ -8,9 +8,11 @@ namespace Drupal\action\Plugin\Action; use Drupal\Component\Utility\Xss; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Utility\Token; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -89,4 +91,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s unset($this->configuration['node']); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/action/src/Tests/BulkFormTest.php b/core/modules/action/src/Tests/BulkFormTest.php index 8fd634b..c108a83 100644 --- a/core/modules/action/src/Tests/BulkFormTest.php +++ b/core/modules/action/src/Tests/BulkFormTest.php @@ -63,6 +63,14 @@ public function testBulkForm() { $edit["node_bulk_form[$i]"] = TRUE; } + // Log in as a user with 'administer nodes' permission to have access to the + // bulk operation. + $this->drupalCreateContentType(['type' => 'page']); + $admin_user = $this->drupalCreateUser(['administer nodes', 'edit any page content']); + $this->drupalLogin($admin_user); + + $this->drupalGet('test_bulk_form'); + // Set all nodes to sticky and check that. $edit += array('action' => 'node_make_sticky_action'); $this->drupalPostForm(NULL, $edit, t('Apply')); diff --git a/core/modules/comment/src/Plugin/Action/PublishComment.php b/core/modules/comment/src/Plugin/Action/PublishComment.php index 41ed873..78fc7fa 100644 --- a/core/modules/comment/src/Plugin/Action/PublishComment.php +++ b/core/modules/comment/src/Plugin/Action/PublishComment.php @@ -8,7 +8,7 @@ namespace Drupal\comment\Plugin\Action; use Drupal\Core\Action\ActionBase; -use Drupal\comment\CommentInterface; +use Drupal\Core\Session\AccountInterface; /** * Publishes a comment. @@ -29,4 +29,15 @@ public function execute($comment = NULL) { $comment->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\comment\CommentInterface $object */ + $result = $object->status->access('edit', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/comment/src/Plugin/Action/SaveComment.php b/core/modules/comment/src/Plugin/Action/SaveComment.php index 5ce763e..eb2ee42 100644 --- a/core/modules/comment/src/Plugin/Action/SaveComment.php +++ b/core/modules/comment/src/Plugin/Action/SaveComment.php @@ -8,6 +8,7 @@ namespace Drupal\comment\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Saves a comment. @@ -27,4 +28,12 @@ public function execute($comment = NULL) { $comment->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\comment\CommentInterface $object */ + return $object->access('update', $account, $return_as_object); + } + } diff --git a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php index 3d10bcb..56706ad 100644 --- a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php +++ b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php @@ -9,8 +9,8 @@ use Drupal\Component\Utility\Tags; use Drupal\Core\Action\ConfigurableActionBase; -use Drupal\comment\CommentInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a comment containing certain keywords. @@ -67,4 +67,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords')); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\comment\CommentInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/comment/src/Plugin/Action/UnpublishComment.php b/core/modules/comment/src/Plugin/Action/UnpublishComment.php index 74d565a..4fb3d56 100644 --- a/core/modules/comment/src/Plugin/Action/UnpublishComment.php +++ b/core/modules/comment/src/Plugin/Action/UnpublishComment.php @@ -8,7 +8,7 @@ namespace Drupal\comment\Plugin\Action; use Drupal\Core\Action\ActionBase; -use Drupal\comment\CommentInterface; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a comment. @@ -29,4 +29,15 @@ public function execute($comment = NULL) { $comment->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\comment\CommentInterface $object */ + $result = $object->status->access('edit', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php index 17e62d1..0d1cc38 100644 --- a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php +++ b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php @@ -11,6 +11,7 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -132,4 +133,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['owner_uid'] = $this->connection->query('SELECT uid from {users_field_data} WHERE name = :name AND default_langcode = 1', array(':name' => $form_state->getValue('owner_name')))->fetchField(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->uid->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/DeleteNode.php b/core/modules/node/src/Plugin/Action/DeleteNode.php index a9ebda9..644630f 100644 --- a/core/modules/node/src/Plugin/Action/DeleteNode.php +++ b/core/modules/node/src/Plugin/Action/DeleteNode.php @@ -9,6 +9,7 @@ use Drupal\Core\Action\ActionBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\user\TempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -70,4 +71,12 @@ public function execute($object = NULL) { $this->executeMultiple(array($object)); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + return $object->access('delete', $account, $return_as_object); + } + } diff --git a/core/modules/node/src/Plugin/Action/DemoteNode.php b/core/modules/node/src/Plugin/Action/DemoteNode.php index e490ecb..4a9fa4e 100644 --- a/core/modules/node/src/Plugin/Action/DemoteNode.php +++ b/core/modules/node/src/Plugin/Action/DemoteNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Demotes a node. @@ -28,4 +29,15 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->promote->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/PromoteNode.php b/core/modules/node/src/Plugin/Action/PromoteNode.php index 0cfc316..a7ca2d2 100644 --- a/core/modules/node/src/Plugin/Action/PromoteNode.php +++ b/core/modules/node/src/Plugin/Action/PromoteNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Promotes a node. @@ -29,4 +30,16 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $status_access = $object->status->access('edit', $account, TRUE); + $promoted_access = $object->promote->access('edit', $account, TRUE); + $update_access = $object->access('update', $account, TRUE); + $access = $status_access->andIf($promoted_access)->andIf($update_access); + return $return_as_object ? $access : $access->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/PublishNode.php b/core/modules/node/src/Plugin/Action/PublishNode.php index 20da55e..f01698f 100644 --- a/core/modules/node/src/Plugin/Action/PublishNode.php +++ b/core/modules/node/src/Plugin/Action/PublishNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Publishes a node. @@ -28,4 +29,15 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/SaveNode.php b/core/modules/node/src/Plugin/Action/SaveNode.php index b758b72..4e56e8a 100644 --- a/core/modules/node/src/Plugin/Action/SaveNode.php +++ b/core/modules/node/src/Plugin/Action/SaveNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Provides an action that can save any entity. @@ -27,4 +28,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + return $object->access('update', $account, $return_as_object); + } + } diff --git a/core/modules/node/src/Plugin/Action/StickyNode.php b/core/modules/node/src/Plugin/Action/StickyNode.php index c4613ce..021161a 100644 --- a/core/modules/node/src/Plugin/Action/StickyNode.php +++ b/core/modules/node/src/Plugin/Action/StickyNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Makes a node sticky. @@ -29,4 +30,16 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $status_access = $object->status->access('edit', $account, TRUE); + $sticky_access = $object->sticky->access('edit', $account, TRUE); + $update_access = $object->access('update', $account, TRUE); + $access = $status_access->andIf($sticky_access)->andIf($update_access); + return $return_as_object ? $access : $access->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php index 60bd836..7684af6 100644 --- a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php +++ b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Tags; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a node containing certain keywords. @@ -65,4 +66,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords')); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/UnpublishNode.php b/core/modules/node/src/Plugin/Action/UnpublishNode.php index d462d6d..3672607 100644 --- a/core/modules/node/src/Plugin/Action/UnpublishNode.php +++ b/core/modules/node/src/Plugin/Action/UnpublishNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a node. @@ -28,4 +29,15 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/UnstickyNode.php b/core/modules/node/src/Plugin/Action/UnstickyNode.php index 204b9d5..1b8cd0b 100644 --- a/core/modules/node/src/Plugin/Action/UnstickyNode.php +++ b/core/modules/node/src/Plugin/Action/UnstickyNode.php @@ -8,6 +8,7 @@ namespace Drupal\node\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Makes a node not sticky. @@ -28,4 +29,15 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\node\NodeInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->sticky->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Tests/Views/BulkFormAccessTest.php b/core/modules/node/src/Tests/Views/BulkFormAccessTest.php new file mode 100644 index 0000000..d0cf35c --- /dev/null +++ b/core/modules/node/src/Tests/Views/BulkFormAccessTest.php @@ -0,0 +1,175 @@ +drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + + $this->accessHandler = \Drupal::entityManager()->getAccessControlHandler('node'); + + node_access_test_add_field(entity_load('node_type', 'article')); + + // After enabling a node access module, the access table has to be rebuild. + node_access_rebuild(); + + // Enable the private node feature of the node_access_test module. + \Drupal::state()->set('node_access_test.private', TRUE); + } + + /** + * Tests if nodes that may not be edited, can not be edited in bulk. + */ + public function testNodeEditAccess() { + // Create an account who will be the author of a private node. + $author = $this->drupalCreateUser(); + // Create a private node (author may view, edit and delete, others may not). + $node = $this->drupalCreateNode(array( + 'type' => 'article', + 'private' => array(array( + 'value' => TRUE, + )), + 'uid' => $author->id(), + )); + // Create an account that may view the private node, but not edit it. + $account = $this->drupalCreateUser(array('node test view')); + $this->drupalLogin($account); + + // Ensure the node is published. + $this->assertTrue($node->isPublished(), 'Node is initially published.'); + + // Ensure that the node can not be edited. + $this->assertEqual(FALSE, $this->accessHandler->access($node, 'update', $node->prepareLangcode(), $account), 'The node may not be edited.'); + + // Test editing the node using the bulk form. + $edit = array( + 'node_bulk_form[0]' => TRUE, + 'action' => 'node_unpublish_action', + ); + $this->drupalPostForm('test-node-bulk-form', $edit, t('Apply')); + $this->assertRaw(String::format('No access to execute %action on the @entity_type_label %entity_label.', [ + '%action' => 'Unpublish content', + '@entity_type_label' => 'Content', + '%entity_label' => $node->label(), + ])); + + // Re-load the node and check the status. + $node = Node::load($node->id()); + $this->assertTrue($node->isPublished(), 'The node is still published.'); + + // Create an account that may view the private node, but can update the + // status. + $account = $this->drupalCreateUser(array('administer nodes', 'edit any article content', 'node test view')); + $this->drupalLogin($account); + + // Ensure the node is published. + $this->assertTrue($node->isPublished(), 'Node is initially published.'); + + // Ensure that the private node can not be edited. + $this->assertEqual(FALSE, $node->access('update', $account), 'The node may not be edited.'); + $this->assertEqual(TRUE, $node->status->access('edit', $account), 'The node can be edited.'); + + // Test editing the node using the bulk form. + $edit = array( + 'node_bulk_form[0]' => TRUE, + 'action' => 'node_unpublish_action', + ); + $this->drupalPostForm('test-node-bulk-form', $edit, t('Apply')); + // Re-load the node and check the status. + $node = Node::load($node->id()); + $this->assertTrue($node->isPublished(), 'The node is still published.'); + } + + /** + * Tests if nodes that may not be deleted, can not be deleted in bulk. + */ + public function testNodeDeleteAccess() { + // Create an account who will be the author of a private node. + $author = $this->drupalCreateUser(); + // Create a private node (author may view, edit and delete, others may not). + $private_node = $this->drupalCreateNode(array( + 'type' => 'article', + 'private' => array(array( + 'value' => TRUE, + )), + 'uid' => $author->id(), + )); + // Create an account that may view the private node, but not delete it. + $account = $this->drupalCreateUser(array('access content', 'administer nodes', 'delete own article content', 'node test view')); + // Create a node that may be deleted too, to ensure the delete confirmation + // page is shown later. In node_access_test.module, nodes may only be + // deleted by the author. + $own_node = $this->drupalCreateNode(array( + 'type' => 'article', + 'private' => array(array( + 'value' => TRUE, + )), + 'uid' => $account->id(), + )); + $this->drupalLogin($account); + + // Ensure that the private node can not be deleted. + $this->assertEqual(FALSE, $this->accessHandler->access($private_node, 'delete', $private_node->prepareLangcode(), $account), 'The private node may not be deleted.'); + // Ensure that the public node may be deleted. + $this->assertEqual(TRUE, $this->accessHandler->access($own_node, 'delete', $own_node->prepareLangcode(), $account), 'The own node may be deleted.'); + + // Try to delete the node using the bulk form. + $edit = array( + 'node_bulk_form[0]' => TRUE, + 'node_bulk_form[1]' => TRUE, + 'action' => 'node_delete_action', + ); + $this->drupalPostForm('test-node-bulk-form', $edit, t('Apply')); + $this->drupalPostForm(NULL, array(), t('Delete')); + // Ensure the private node still exists. + $private_node = Node::load($private_node->id()); + $this->assertNotNull($private_node, 'The private node has not been deleted.'); + // Ensure the own node is deleted. + $own_node = Node::load($own_node->id()); + $this->assertNull($own_node, 'The own node is deleted.'); + } +} diff --git a/core/modules/node/src/Tests/Views/BulkFormTest.php b/core/modules/node/src/Tests/Views/BulkFormTest.php index 7da2042..42dc34a 100644 --- a/core/modules/node/src/Tests/Views/BulkFormTest.php +++ b/core/modules/node/src/Tests/Views/BulkFormTest.php @@ -29,7 +29,7 @@ class BulkFormTest extends NodeTestBase { */ public function testBulkForm() { $node_storage = $this->container->get('entity.manager')->getStorage('node'); - $this->drupalLogin($this->drupalCreateUser(array('administer nodes'))); + $this->drupalLogin($this->drupalCreateUser(array('administer nodes', 'access content overview', 'bypass node access'))); $node = $this->drupalCreateNode(array( 'promote' => FALSE, )); diff --git a/core/modules/system/src/Plugin/views/field/BulkForm.php b/core/modules/system/src/Plugin/views/field/BulkForm.php index 06a7d3a..7d42b70 100644 --- a/core/modules/system/src/Plugin/views/field/BulkForm.php +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php @@ -15,6 +15,7 @@ use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * Defines a actions-based bulk operation form element. @@ -33,7 +34,7 @@ class BulkForm extends FieldPluginBase { /** * An array of actions that can be executed. * - * @var array + * @var \Drupal\system\ActionConfigEntityInterface[] */ protected $actions = array(); @@ -249,18 +250,35 @@ protected function getBulkOptions($filtered = TRUE) { * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the user tried to access an action without access to it. */ public function viewsFormSubmit(&$form, FormStateInterface $form_state) { if ($form_state->get('step') == 'views_form_views_form') { // Filter only selected checkboxes. $selected = array_filter($form_state->getValue($this->options['id'])); $entities = array(); + $action = $this->actions[$form_state->getValue('action')]; + $count = 0; foreach (array_intersect_key($this->view->result, $selected) as $row) { $entity = $this->getEntity($row); + + // Skip execution if the user did not had access. + if (!$action->getPlugin()->access($entity, $this->view->getUser())) { + $this->drupalSetMessage($this->t('No access to execute %action on the @entity_type_label %entity_label.', [ + '%action' => $action->label(), + '@entity_type_label' => $entity->getEntityType()->getLabel(), + '%entity_label' => $entity->label() + ]), 'error'); + continue; + } + + $count++; + $entities[$entity->id()] = $entity; } - $action = $this->actions[$form_state->getValue('action')]; $action->execute($entities); $operation_definition = $action->getPluginDefinition(); @@ -268,7 +286,6 @@ public function viewsFormSubmit(&$form, FormStateInterface $form_state) { $form_state->setRedirect($operation_definition['confirm_form_route_name']); } - $count = count(array_filter($form_state->getValue($this->options['id']))); if ($count) { drupal_set_message($this->formatPlural($count, '%action was applied to @count item.', '%action was applied to @count items.', array( '%action' => $action->label(), @@ -311,4 +328,11 @@ public function clickSortable() { return FALSE; } + /** + * Wraps drupal_set_message(). + */ + protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { + drupal_set_message($message, $type, $repeat); + } + } diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php index 7f18b57..aa92076 100644 --- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php +++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/NoType.php @@ -7,7 +7,9 @@ namespace Drupal\action_test\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Provides an operation with no type specified. @@ -25,4 +27,12 @@ class NoType extends ActionBase { public function execute($entity = NULL) { } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php index e3d296f..f26af5a 100644 --- a/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php +++ b/core/modules/system/tests/modules/action_test/src/Plugin/Action/SaveEntity.php @@ -8,6 +8,7 @@ namespace Drupal\action_test\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Provides an operation to save user entities. @@ -27,4 +28,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\Core\Entity\EntityInterface $object */ + return $object->access('update', $account, $return_as_object); + } + } diff --git a/core/modules/user/src/Plugin/Action/AddRoleUser.php b/core/modules/user/src/Plugin/Action/AddRoleUser.php index acf4dd9..0ff48c6 100644 --- a/core/modules/user/src/Plugin/Action/AddRoleUser.php +++ b/core/modules/user/src/Plugin/Action/AddRoleUser.php @@ -7,6 +7,8 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Session\AccountInterface; use Drupal\user\Plugin\Action\ChangeUserRoleBase; /** diff --git a/core/modules/user/src/Plugin/Action/BlockUser.php b/core/modules/user/src/Plugin/Action/BlockUser.php index a488f31..a8d51dd 100644 --- a/core/modules/user/src/Plugin/Action/BlockUser.php +++ b/core/modules/user/src/Plugin/Action/BlockUser.php @@ -8,6 +8,7 @@ namespace Drupal\user\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Blocks a user. @@ -34,4 +35,15 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\user\UserInterface $object */ + $result = $object->status->access('edit', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/CancelUser.php b/core/modules/user/src/Plugin/Action/CancelUser.php index 6c0e392..401d00a 100644 --- a/core/modules/user/src/Plugin/Action/CancelUser.php +++ b/core/modules/user/src/Plugin/Action/CancelUser.php @@ -9,6 +9,7 @@ use Drupal\Core\Action\ActionBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\user\TempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -70,4 +71,12 @@ public function execute($object = NULL) { $this->executeMultiple(array($object)); } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\user\UserInterface $object */ + return $object->access('delete', $account, $return_as_object); + } + } diff --git a/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php b/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php index 5e5c176..8981c23 100644 --- a/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php +++ b/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -91,4 +92,15 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\user\UserInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->roles->access('edit', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php index e63a70a..a0f7bdb 100644 --- a/core/modules/user/src/Plugin/Action/RemoveRoleUser.php +++ b/core/modules/user/src/Plugin/Action/RemoveRoleUser.php @@ -7,6 +7,8 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Session\AccountInterface; use Drupal\user\Plugin\Action\ChangeUserRoleBase; /** diff --git a/core/modules/user/src/Plugin/Action/UnblockUser.php b/core/modules/user/src/Plugin/Action/UnblockUser.php index 9c30ebc..f9ca68a 100644 --- a/core/modules/user/src/Plugin/Action/UnblockUser.php +++ b/core/modules/user/src/Plugin/Action/UnblockUser.php @@ -8,6 +8,7 @@ namespace Drupal\user\Plugin\Action; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Unblocks a user. @@ -31,4 +32,15 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\user\UserInterface $object */ + $result = $object->status->access('edit', $account, TRUE) + ->andIf($object->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Tests/Views/BulkFormAccessTest.php b/core/modules/user/src/Tests/Views/BulkFormAccessTest.php new file mode 100644 index 0000000..5044098 --- /dev/null +++ b/core/modules/user/src/Tests/Views/BulkFormAccessTest.php @@ -0,0 +1,140 @@ +drupalCreateUser(array(), 'no_edit'); + // Ensure this account is not blocked. + $this->assertFalse($no_edit_user->isBlocked(), 'The user is not blocked.'); + + // Login as user admin. + $admin_user = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($admin_user); + + // Ensure that the account "no_edit" can not be edited. + $this->drupalGet('user/' . $no_edit_user->id() . '/edit'); + $this->assertFalse($no_edit_user->access('update', $admin_user)); + $this->assertResponse(403, 'The user may not be edited.'); + + // Test blocking the account "no_edit". + $edit = array( + 'user_bulk_form[' . ($no_edit_user->id() -1) . ']' => TRUE, + 'action' => 'user_block_user_action', + ); + $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply')); + $this->assertResponse(200); + + $this->assertRaw(String::format('No access to execute %action on the @entity_type_label %entity_label.', [ + '%action' => 'Block the selected user(s)', + '@entity_type_label' => 'User', + '%entity_label' => $no_edit_user->label(), + ])); + debug(String::format('No access to execute %action on the @entity_type_label %entity_label.', [ + '%action' => 'Block the selected user(s)', + '@entity_type_label' => 'User', + '%entity_label' => $no_edit_user->label(), + ])); + + // Re-load the account "no_edit" and ensure it is not blocked. + $no_edit_user = User::load($no_edit_user->id()); + $this->assertFalse($no_edit_user->isBlocked(), 'The user is not blocked.'); + + // Create a normal user which can be edited by the admin user + $normal_user = $this->drupalCreateUser(); + $this->assertTrue($normal_user->access('update', $admin_user)); + + $edit = array( + 'user_bulk_form[' . ($normal_user->id() -1) . ']' => TRUE, + 'action' => 'user_block_user_action', + ); + $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply')); + + $normal_user = User::load($normal_user->id()); + $this->assertTrue($normal_user->isBlocked(), 'The user is blocked.'); + + // Login as user without the 'administer users' permission. + $this->drupalLogin($this->drupalCreateUser()); + + $edit = array( + 'user_bulk_form[' . ($normal_user->id() -1) . ']' => TRUE, + 'action' => 'user_unblock_user_action', + ); + $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply')); + + // Re-load the normal user and ensure it is still blocked. + $normal_user = User::load($normal_user->id()); + $this->assertTrue($normal_user->isBlocked(), 'The user is still blocked.'); + } + + /** + * Tests if users that may not be deleted, can not be deleted in bulk. + */ + public function testUserDeleteAccess() { + // Create two authenticated users. + $account = $this->drupalCreateUser(array(), 'no_delete'); + $account2 = $this->drupalCreateUser(array(), 'may_delete'); + + // Login as user admin. + $this->drupalLogin($this->drupalCreateUser(array('administer users'))); + + // Ensure that the account "no_delete" can not be deleted. + $this->drupalGet('user/' . $account->id() . '/cancel'); + $this->assertResponse(403, 'The user "no_delete" may not be deleted.'); + // Ensure that the account "may_delete" *can* be deleted. + $this->drupalGet('user/' . $account2->id() . '/cancel'); + $this->assertResponse(200, 'The user "may_delete" may be deleted.'); + + // Test deleting the accounts "no_delete" and "may_delete". + $edit = array( + 'user_bulk_form[' . ($account->id() -1) . ']' => TRUE, + 'user_bulk_form[' . ($account2->id() -1) . ']' => TRUE, + 'action' => 'user_cancel_user_action', + ); + $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply')); + $edit = array( + 'user_cancel_method' => 'user_cancel_delete', + ); + $this->drupalPostForm(NULL, $edit, t('Cancel accounts')); + + // Ensure the account "no_delete" still exists. + $account = User::load($account->id()); + $this->assertNotNull($account, 'The user "no_delete" is not deleted.'); + // Ensure the account "may_delete" no longer exists. + $account = User::load($account2->id()); + $this->assertNull($account, 'The user "may_delete" is deleted.'); + } +} diff --git a/core/modules/user/src/Tests/Views/BulkFormTest.php b/core/modules/user/src/Tests/Views/BulkFormTest.php index 289c1ea..b1c95d7 100644 --- a/core/modules/user/src/Tests/Views/BulkFormTest.php +++ b/core/modules/user/src/Tests/Views/BulkFormTest.php @@ -35,8 +35,15 @@ class BulkFormTest extends UserTestBase { * Tests the user bulk form. */ public function testBulkForm() { + // Login as a user without 'administer users. $this->drupalLogin($this->drupalCreateUser(array('administer permissions'))); + // Create an user which actually can change users. + $this->drupalLogin($this->drupalCreateUser(array('administer users'))); + $this->drupalGet('test-user-bulk-form'); + $result = $this->cssSelect('#edit-action option'); + $this->assertTrue(count($result) > 0); + // Test submitting the page with no selection. $edit = array( 'action' => 'user_block_user_action', @@ -101,7 +108,7 @@ public function testBulkForm() { $this->assertTrue($anonymous_account->isBlocked(), 'Ensure the anonymous user got blocked.'); // Test the list of available actions with a value that contains a dot. - $this->drupalLogin($this->drupalCreateUser(array('administer permissions', 'administer views'))); + $this->drupalLogin($this->drupalCreateUser(array('administer permissions', 'administer views', 'administer users'))); $action_id = 'user_add_role_action.' . $role; $edit = [ 'options[include_exclude]' => 'exclude', diff --git a/core/modules/user/tests/modules/user_access_test/user_access_test.info.yml b/core/modules/user/tests/modules/user_access_test/user_access_test.info.yml new file mode 100644 index 0000000..5b13963 --- /dev/null +++ b/core/modules/user/tests/modules/user_access_test/user_access_test.info.yml @@ -0,0 +1,6 @@ +name: 'User access tests' +type: module +description: 'Support module for user access testing.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/user/tests/modules/user_access_test/user_access_test.module b/core/modules/user/tests/modules/user_access_test/user_access_test.module new file mode 100644 index 0000000..470a76a --- /dev/null +++ b/core/modules/user/tests/modules/user_access_test/user_access_test.module @@ -0,0 +1,24 @@ +getUsername() == "no_edit" && $operation == "update") { + // Deny edit access. + return AccessResult::forbidden(); + } + if ($entity->getUsername() == "no_delete" && $operation == "delete") { + // Deny delete access. + return AccessResult::forbidden(); + } + return AccessResult::neutral(); +} diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php index d0fa62f..c2be665 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php @@ -29,6 +29,9 @@ protected function defineOptions() { return $options; } + /** + * {@inheritdoc} + */ public function access(AccountInterface $account) { return !empty($this->options['access']); }