From 7c0c951f76f0c78179a9ffb12fac2cbbac489cb2 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Mon, 11 Oct 2021 09:08:35 +0100 Subject: [PATCH] Issue #3228000 by bbrala, larowlan, GuyPaddock, Wim Leers, bradjones1, alexpott, catch, e0ipso: Users deleted via JSON:API DELETE don't follow the site-wide cancel_method in the user settings (cherry picked from commit 9d8e7da4fc89c514c1059f4bb1fa3ee72d6d508d) --- .../jsonapi/src/Controller/EntityResource.php | 20 +- .../jsonapi/tests/src/Functional/UserTest.php | 197 +++++++++++++++++- 2 files changed, 215 insertions(+), 2 deletions(-) diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php index 47bc9c056de..ca588d12f7a 100644 --- a/core/modules/jsonapi/src/Controller/EntityResource.php +++ b/core/modules/jsonapi/src/Controller/EntityResource.php @@ -369,7 +369,25 @@ class EntityResource { * The response. */ public function deleteIndividual(EntityInterface $entity) { - $entity->delete(); + // @todo Replace with entity handlers in: https://www.drupal.org/project/drupal/issues/3230434 + if ($entity->getEntityTypeId() === 'user') { + $cancel_method = \Drupal::service('config.factory')->get('user.settings')->get('cancel_method'); + + // Allow other modules to act. + + user_cancel([], $entity->id(), $cancel_method); + // Since user_cancel() is not invoked via Form API, batch processing + // needs to be invoked manually. + $batch =& batch_get(); + // Mark this batch as non-progressive to bypass the progress bar and + // redirect. + $batch['progressive'] = FALSE; + batch_process(); + } + else { + $entity->delete(); + } + return new ResourceResponse(NULL, 204); } diff --git a/core/modules/jsonapi/tests/src/Functional/UserTest.php b/core/modules/jsonapi/tests/src/Functional/UserTest.php index bc6b453b431..2184c3bcb4b 100644 --- a/core/modules/jsonapi/tests/src/Functional/UserTest.php +++ b/core/modules/jsonapi/tests/src/Functional/UserTest.php @@ -10,6 +10,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\node\Entity\Node; use Drupal\user\Entity\User; +use Drupal\user\UserInterface; use GuzzleHttp\RequestOptions; /** @@ -19,10 +20,12 @@ use GuzzleHttp\RequestOptions; */ class UserTest extends ResourceTestBase { + const BATCH_TEST_NODE_COUNT = 15; + /** * {@inheritdoc} */ - protected static $modules = ['user', 'jsonapi_test_user']; + protected static $modules = ['user', 'jsonapi_test_user', 'node']; /** * {@inheritdoc} @@ -119,6 +122,15 @@ class UserTest extends ResourceTestBase { return $user; } + /** + * {@inheritdoc} + */ + public function testDeleteIndividual() { + $this->config('user.settings')->set('cancel_method', 'user_cancel_delete')->save(TRUE); + + parent::testDeleteIndividual(); + } + /** * {@inheritdoc} */ @@ -597,6 +609,175 @@ class UserTest extends ResourceTestBase { $this->assertEquals($original_name, $updated_user->get('name')->value); } + /** + * Tests if JSON:API respects user.settings.cancel_method: user_cancel_block. + */ + public function testDeleteRespectsUserCancelBlock() { + $cancel_method = 'user_cancel_block'; + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + $this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE); + + $account = $this->createAnotherEntity($cancel_method); + $node = $this->drupalCreateNode(['uid' => $account->id()]); + + $this->sendDeleteRequestForUser($account, $cancel_method); + + $user_storage = $this->container->get('entity_type.manager') + ->getStorage('user'); + $user_storage->resetCache([$account->id()]); + $account = $user_storage->load($account->id()); + + $this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + $this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $node_storage->resetCache([$node->id()]); + $test_node = $node_storage->load($node->id()); + $this->assertNotNull($test_node, 'Node of the user is not deleted.'); + $this->assertTrue($test_node->isPublished(), 'Node of the user is published.'); + $test_node = node_revision_load($node->getRevisionId()); + $this->assertTrue($test_node->isPublished(), 'Node revision of the user is published.'); + } + + /** + * Tests if JSON:API respects user.settings.cancel_method: user_cancel_block_unpublish. + */ + public function testDeleteRespectsUserCancelBlockUnpublish() { + $cancel_method = 'user_cancel_block_unpublish'; + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + $this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE); + + $account = $this->createAnotherEntity($cancel_method); + $node = $this->drupalCreateNode(['uid' => $account->id()]); + + $this->sendDeleteRequestForUser($account, $cancel_method); + + $user_storage = $this->container->get('entity_type.manager') + ->getStorage('user'); + $user_storage->resetCache([$account->id()]); + $account = $user_storage->load($account->id()); + + $this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + $this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $node_storage->resetCache([$node->id()]); + $test_node = $node_storage->load($node->id()); + $this->assertNotNull($test_node, 'Node of the user is not deleted.'); + $this->assertFalse($test_node->isPublished(), 'Node of the user is no longer published.'); + $test_node = node_revision_load($node->getRevisionId()); + $this->assertFalse($test_node->isPublished(), 'Node revision of the user is no longer published.'); + } + + /** + * Tests if JSON:API respects user.settings.cancel_method: user_cancel_block_unpublish. + * @group jsonapi + */ + public function testDeleteRespectsUserCancelBlockUnpublishAndProcessesBatches() { + $cancel_method = 'user_cancel_block_unpublish'; + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + $this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE); + + $account = $this->createAnotherEntity($cancel_method); + + $nodeCount = self::BATCH_TEST_NODE_COUNT; + $node_ids = []; + $nodes = []; + while ($nodeCount-- > 0) { + $node = $this->drupalCreateNode(['uid' => $account->id()]); + $nodes[] = $node; + $node_ids[] = $node->id(); + } + + $this->sendDeleteRequestForUser($account, $cancel_method); + + $user_storage = $this->container->get('entity_type.manager') + ->getStorage('user'); + $user_storage->resetCache([$account->id()]); + $account = $user_storage->load($account->id()); + + $this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + $this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $node_storage->resetCache($node_ids); + + $test_nodes = $node_storage->loadMultiple($node_ids); + + $this->assertCount(self::BATCH_TEST_NODE_COUNT, $test_nodes, 'Nodes of the user are not deleted.'); + + foreach ($test_nodes as $test_node) { + $this->assertFalse($test_node->isPublished(), 'Node of the user is no longer published.'); + } + + foreach ($nodes as $node) { + $test_node = node_revision_load($node->getRevisionId()); + $this->assertFalse($test_node->isPublished(), 'Node revision of the user is no longer published.'); + } + } + + /** + * Tests if JSON:API respects user.settings.cancel_method: user_cancel_reassign. + */ + public function testDeleteRespectsUserCancelReassign() { + $cancel_method = 'user_cancel_reassign'; + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + $this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE); + + $account = $this->createAnotherEntity($cancel_method); + $node = $this->drupalCreateNode(['uid' => $account->id()]); + + $this->sendDeleteRequestForUser($account, $cancel_method); + + $user_storage = $this->container->get('entity_type.manager') + ->getStorage('user'); + $user_storage->resetCache([$account->id()]); + $account = $user_storage->load($account->id()); + + $this->assertNull($account, 'User is deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $node_storage->resetCache([$node->id()]); + $test_node = $node_storage->load($node->id()); + $this->assertNotNull($test_node, 'Node of the user is not deleted.'); + $this->assertTrue($test_node->isPublished(), 'Node of the user is still published.'); + $this->assertEquals(0, $test_node->getOwnerId(), 'Node of the user has been attributed to anonymous user.'); + $test_node = node_revision_load($node->getRevisionId()); + $this->assertTrue($test_node->isPublished(), 'Node revision of the user is still published.'); + $this->assertEquals(0, $test_node->getRevisionUser()->id(), 'Node revision of the user has been attributed to anonymous user.'); + } + + /** + * Tests if JSON:API respects user.settings.cancel_method: user_cancel_delete. + */ + public function testDeleteRespectsUserCancelDelete() { + $cancel_method = 'user_cancel_delete'; + $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); + $this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE); + + $account = $this->createAnotherEntity($cancel_method); + $node = $this->drupalCreateNode(['uid' => $account->id()]); + + $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $account->uuid()]); + $request_options = []; + $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions()); + $this->setUpAuthorization('DELETE'); + $response = $this->request('DELETE', $url, $request_options); + $this->assertResourceResponse(204, NULL, $response); + + $node_storage = $this->container->get('entity_type.manager')->getStorage('node'); + $user_storage = $this->container->get('entity_type.manager')->getStorage('user'); + + $user_storage->resetCache([$account->id()]); + $account = $user_storage->load($account->id()); + $this->assertNull($account, 'User is deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method); + + $node_storage->resetCache([$node->id()]); + $test_node = $node_storage->load($node->id()); + $this->assertNull($test_node, 'Node of the user is deleted.'); + } + /** * {@inheritdoc} */ @@ -620,4 +801,18 @@ class UserTest extends ResourceTestBase { return parent::makeNormalizationInvalid($document, $entity_key); } + /** + * @param \Drupal\user\UserInterface $account + * @param string $cancel_method + */ + private function sendDeleteRequestForUser(UserInterface $account, string $cancel_method) { + $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $account->uuid()]); + $request_options = []; + $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; + $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions()); + $this->setUpAuthorization('DELETE'); + $response = $this->request('DELETE', $url, $request_options); + $this->assertResourceResponse(204, NULL, $response); + } + }