diff --git a/core/lib/Drupal/Core/Action/ActionInterface.php b/core/lib/Drupal/Core/Action/ActionInterface.php index fe04acb..21a5c5e 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\Access\AccessibleInterface; /** * Provides an interface for an Action plugin. @@ -34,7 +35,7 @@ * @see \Drupal\Core\Action\ActionBase * @see plugin_api */ -interface ActionInterface extends ExecutableInterface, PluginInspectionInterface { +interface ActionInterface extends ExecutableInterface, PluginInspectionInterface, AccessibleInterface { /** * Executes the plugin for an array of objects. diff --git a/core/modules/action/src/Plugin/Action/EmailAction.php b/core/modules/action/src/Plugin/Action/EmailAction.php index 5bdc609..545d051 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($operation, 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..e09890c 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($operation, 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..a8e9f8c 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($operation, 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..f04b9ce 100644 --- a/core/modules/action/src/Tests/BulkFormTest.php +++ b/core/modules/action/src/Tests/BulkFormTest.php @@ -63,6 +63,17 @@ public function testBulkForm() { $edit["node_bulk_form[$i]"] = TRUE; } + // As a anonymous user you should not have actions available. + $result = $this->cssSelect('#edit-action option'); + $this->assertEqual(0, count($result)); + + // Login as a user with 'administer nodes' permission to have access + // to the bulk operation. + $admin_user = $this->drupalCreateUser(['administer nodes']); + $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..d23a700 100644 --- a/core/modules/comment/src/Plugin/Action/PublishComment.php +++ b/core/modules/comment/src/Plugin/Action/PublishComment.php @@ -7,8 +7,10 @@ namespace Drupal\comment\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; use Drupal\comment\CommentInterface; +use Drupal\Core\Session\AccountInterface; /** * Publishes a comment. @@ -29,4 +31,12 @@ public function execute($comment = NULL) { $comment->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer comments'); + 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..55faa47 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($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer comments'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php index 3d10bcb..99e75d3 100644 --- a/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php +++ b/core/modules/comment/src/Plugin/Action/UnpublishByKeywordComment.php @@ -8,9 +8,11 @@ namespace Drupal\comment\Plugin\Action; use Drupal\Component\Utility\Tags; +use Drupal\Core\Access\AccessResult; 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 +69,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords')); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer comments'); + 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..f2403b1 100644 --- a/core/modules/comment/src/Plugin/Action/UnpublishComment.php +++ b/core/modules/comment/src/Plugin/Action/UnpublishComment.php @@ -7,8 +7,10 @@ namespace Drupal\comment\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; use Drupal\comment\CommentInterface; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a comment. @@ -29,4 +31,12 @@ public function execute($comment = NULL) { $comment->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer comments'); + 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..eeba913 100644 --- a/core/modules/node/src/Plugin/Action/AssignOwnerNode.php +++ b/core/modules/node/src/Plugin/Action/AssignOwnerNode.php @@ -7,10 +7,12 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; 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 +134,12 @@ 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($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..53c1cbb 100644 --- a/core/modules/node/src/Plugin/Action/DeleteNode.php +++ b/core/modules/node/src/Plugin/Action/DeleteNode.php @@ -7,8 +7,10 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; 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 +72,12 @@ public function execute($object = NULL) { $this->executeMultiple(array($object)); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/DemoteNode.php b/core/modules/node/src/Plugin/Action/DemoteNode.php index e490ecb..86e3d00 100644 --- a/core/modules/node/src/Plugin/Action/DemoteNode.php +++ b/core/modules/node/src/Plugin/Action/DemoteNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Demotes a node. @@ -28,4 +30,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..4204616 100644 --- a/core/modules/node/src/Plugin/Action/PromoteNode.php +++ b/core/modules/node/src/Plugin/Action/PromoteNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Promotes a node. @@ -29,4 +31,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/PublishNode.php b/core/modules/node/src/Plugin/Action/PublishNode.php index 20da55e..0a7f518 100644 --- a/core/modules/node/src/Plugin/Action/PublishNode.php +++ b/core/modules/node/src/Plugin/Action/PublishNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Publishes a node. @@ -28,4 +30,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..0bd8fca 100644 --- a/core/modules/node/src/Plugin/Action/SaveNode.php +++ b/core/modules/node/src/Plugin/Action/SaveNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Provides an action that can save any entity. @@ -27,4 +29,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/StickyNode.php b/core/modules/node/src/Plugin/Action/StickyNode.php index c4613ce..435fd81 100644 --- a/core/modules/node/src/Plugin/Action/StickyNode.php +++ b/core/modules/node/src/Plugin/Action/StickyNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Makes a node sticky. @@ -29,4 +31,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php b/core/modules/node/src/Plugin/Action/UnpublishByKeywordNode.php index 60bd836..706f97d 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,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords')); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..5054824 100644 --- a/core/modules/node/src/Plugin/Action/UnpublishNode.php +++ b/core/modules/node/src/Plugin/Action/UnpublishNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Unpublishes a node. @@ -28,4 +30,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..cb0c837 100644 --- a/core/modules/node/src/Plugin/Action/UnstickyNode.php +++ b/core/modules/node/src/Plugin/Action/UnstickyNode.php @@ -7,7 +7,9 @@ namespace Drupal\node\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Makes a node not sticky. @@ -28,4 +30,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer nodes'); + 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..ed622fd --- /dev/null +++ b/core/modules/node/src/Tests/Views/BulkFormAccessTest.php @@ -0,0 +1,145 @@ +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('administer nodes', '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')); + // Re-load the node and check the status. + $node = entity_load('node', $node->id(), TRUE); + $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', '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 = entity_load('node', $private_node->id(), TRUE); + $this->assertNotNull($private_node, 'The private node has not been deleted.'); + // Ensure the own node is deleted. + $own_node = entity_load('node', $own_node->id(), TRUE); + $this->assertNull($own_node, 'The own node is deleted.'); + } +} diff --git a/core/modules/system/src/Entity/Action.php b/core/modules/system/src/Entity/Action.php index ca3d8cf..4174dd9 100644 --- a/core/modules/system/src/Entity/Action.php +++ b/core/modules/system/src/Entity/Action.php @@ -21,6 +21,9 @@ * id = "action", * label = @Translation("Action"), * admin_permission = "administer actions", + * handlers = { + * "access" = "Drupal\system\Entity\ActionAccessControlHandler", + * }, * entity_keys = { * "id" = "id", * "label" = "label" diff --git a/core/modules/system/src/Entity/ActionAccessControlHandler.php b/core/modules/system/src/Entity/ActionAccessControlHandler.php new file mode 100644 index 0000000..8d336f8 --- /dev/null +++ b/core/modules/system/src/Entity/ActionAccessControlHandler.php @@ -0,0 +1,27 @@ +getPlugin()->access($operation, $account, TRUE); + } + +} diff --git a/core/modules/system/src/Plugin/views/field/BulkForm.php b/core/modules/system/src/Plugin/views/field/BulkForm.php index 06a7d3a..8fcf9df 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\Core\Action\ActionInterface[] */ protected $actions = array(); @@ -223,6 +224,12 @@ protected function getBulkOptions($filtered = TRUE) { // Filter the action list. foreach ($this->actions as $id => $action) { if ($filtered) { + + // Filter out options without access to it. + // @fixme Find out the right operation. + if (!$action->access('execute', $this->view->getUser())) { + continue; + } $in_selected = in_array($id, $this->options['selected_actions']); // If the field is configured to include only the selected actions, // skip actions that were not selected. @@ -249,6 +256,9 @@ 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') { @@ -261,6 +271,11 @@ public function viewsFormSubmit(&$form, FormStateInterface $form_state) { } $action = $this->actions[$form_state->getValue('action')]; + + if (!$action->access('execute', $this->view->getUser())) { + throw new AccessDeniedHttpException(); + } + $action->execute($entities); $operation_definition = $action->getPluginDefinition(); 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..21ae399c 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($operation, 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..98e94bd 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 @@ -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 to save user entities. @@ -27,4 +29,12 @@ public function execute($entity = NULL) { $entity->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users', 'OR'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/AddRoleUser.php b/core/modules/user/src/Plugin/Action/AddRoleUser.php index acf4dd9..7e66646 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; /** @@ -35,4 +37,12 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/BlockUser.php b/core/modules/user/src/Plugin/Action/BlockUser.php index a488f31..6e4ddbe 100644 --- a/core/modules/user/src/Plugin/Action/BlockUser.php +++ b/core/modules/user/src/Plugin/Action/BlockUser.php @@ -7,7 +7,9 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Blocks a user. @@ -34,4 +36,12 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + 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..e9e2940 100644 --- a/core/modules/user/src/Plugin/Action/CancelUser.php +++ b/core/modules/user/src/Plugin/Action/CancelUser.php @@ -7,8 +7,10 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; 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 +72,12 @@ public function execute($object = NULL) { $this->executeMultiple(array($object)); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php b/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php index 5e5c176..3f49183 100644 --- a/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php +++ b/core/modules/user/src/Plugin/Action/ChangeUserRoleBase.php @@ -7,11 +7,13 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Entity\DependencyTrait; 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 +93,12 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + 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..03676b3 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; /** @@ -35,4 +37,12 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/user/src/Plugin/Action/UnblockUser.php b/core/modules/user/src/Plugin/Action/UnblockUser.php index 9c30ebc..9eb3328 100644 --- a/core/modules/user/src/Plugin/Action/UnblockUser.php +++ b/core/modules/user/src/Plugin/Action/UnblockUser.php @@ -7,7 +7,9 @@ namespace Drupal\user\Plugin\Action; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; +use Drupal\Core\Session\AccountInterface; /** * Unblocks a user. @@ -31,4 +33,12 @@ public function execute($account = NULL) { } } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'administer users'); + 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..16e6942 --- /dev/null +++ b/core/modules/user/src/Tests/Views/BulkFormAccessTest.php @@ -0,0 +1,98 @@ +drupalCreateUser(array(), 'no_edit'); + // Ensure this account is not blocked. + $this->assertFalse($account->isBlocked(), 'The user is not blocked.'); + + // Login as user admin. + $this->drupalLogin($this->drupalCreateUser(array('administer users'))); + + // Ensure that the account "no_edit" can not be edited. + $this->drupalGet('user/' . $account->id() . '/edit'); + $this->assertResponse(403, 'The user may not be edited.'); + + // Test blocking the account "no_edit". + $edit = array( + 'user_bulk_form[' . ($account->id() -1) . ']' => TRUE, + 'action' => 'user_block_user_action', + ); + $this->drupalPostForm('test-user-bulk-form', $edit, t('Apply')); + + // Re-load the account "no_edit" and ensure it is still not blocked. + $account = entity_load('user', $account->id(), TRUE); + $this->assertFalse($account->isBlocked(), 'The user is not 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 = entity_load('user', $account->id(), TRUE); + $this->assertNotNull($account, 'The user "no_delete" is not deleted.'); + // Ensure the account "may_delete" no longer exists. + $account = entity_load('user', $account2->id(), TRUE); + $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 2bf721b..dc48baf 100644 --- a/core/modules/user/src/Tests/Views/BulkFormTest.php +++ b/core/modules/user/src/Tests/Views/BulkFormTest.php @@ -35,8 +35,20 @@ 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'))); + // Ensure that there are no actions available. + $this->drupalGet('test-user-bulk-form'); + $result = $this->cssSelect('#edit-action option'); + $this->assertEqual(0, count($result)); + + // 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 +113,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']); }