Skip to content
Snippets Groups Projects
Commit 8f2fac94 authored by Joshua Sedler's avatar Joshua Sedler :cartwheel_tone2: Committed by Julian Pustkuchen
Browse files

Issue #3156407 by Grevil, samuelpodina: User associated contents are deleted

parent 25fe1352
No related branches found
No related tags found
1 merge request!17Issue #3156407: User associated contents are deleted
services:
purge_users.user_management:
class: Drupal\purge_users\Services\UserManagementService
arguments: ['@current_user', '@config.factory', '@module_handler', '@messenger', '@logger.factory', '@database']
arguments: ['@current_user', '@config.factory', '@module_handler', '@messenger', '@logger.factory', '@database', '@entity_type.manager']
......@@ -12,6 +12,9 @@ use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\user\UserInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Class that holds the purging logic.
......@@ -62,6 +65,13 @@ class UserManagementService implements UserManagementServiceInterface {
*/
protected $connection;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* UserManagementService constructor.
*
......@@ -77,14 +87,17 @@ class UserManagementService implements UserManagementServiceInterface {
* The logger factory.
* @param \Drupal\Core\Database\Connection $connection
* The db connection.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, MessengerInterface $messenger, LoggerChannelFactoryInterface $logger_factory, Connection $connection) {
public function __construct(AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, MessengerInterface $messenger, LoggerChannelFactoryInterface $logger_factory, Connection $connection, EntityTypeManagerInterface $entity_type_manager) {
$this->currentUser = $current_user;
$this->config = $config_factory;
$this->moduleHandler = $module_handler;
$this->messenger = $messenger;
$this->loggerFactory = $logger_factory;
$this->connection = $connection;
$this->entityTypeManager = $entity_type_manager;
}
/**
......@@ -106,9 +119,32 @@ class UserManagementService implements UserManagementServiceInterface {
}
switch ($method) {
case 'user_cancel_reassign':
// Reassign content to anonymous:
$this->reassignContentOwnershipToAnonymous($user);
// Notify and delete the user:
$this->notifyUserToPurge($user);
$user->delete();
$this->messenger->addStatus(t('%name has been deleted.', ['%name' => $user->getDisplayName()]));
$logger->notice('Deleted user: %name %email.', [
'%name' => $user->getAccountName(),
'%email' => '<' . $user->getEmail() . '>',
]);
break;
case 'user_cancel_delete':
// Notify and delete the user:
$this->notifyUserToPurge($user);
$user->delete();
$this->messenger->addStatus(t('%name has been deleted.', ['%name' => $user->getDisplayName()]));
$logger->notice('Deleted user: %name %email.', [
'%name' => $user->getAccountName(),
'%email' => '<' . $user->getEmail() . '>',
]);
break;
case 'user_cancel_block':
case 'user_cancel_block_unpublish':
default:
if ($user->isBlocked()) {
// The user is already blocked. Do not block them again.
return;
......@@ -123,16 +159,9 @@ class UserManagementService implements UserManagementServiceInterface {
]);
break;
case 'user_cancel_reassign':
case 'user_cancel_delete':
$this->notifyUserToPurge($user);
$user->delete();
$this->messenger->addStatus(t('%name has been deleted.', ['%name' => $user->getDisplayName()]));
$logger->notice('Deleted user: %name %email.', [
'%name' => $user->getAccountName(),
'%email' => '<' . $user->getEmail() . '>',
]);
break;
default:
throw new \Exception('Unknown user cancel method "' . $method . '"');
}
// After cancelling account, ensure that user is logged out. We can't
......@@ -227,4 +256,65 @@ class UserManagementService implements UserManagementServiceInterface {
->execute();
}
/**
* Reassigns content ownership from a user to "anonymous".
*
* This is currently based on custom logic, scanning the database
* tables (from ::getTablesWithUidColumn()) for the "uid" column and
* updating the value from the user's UId to "0" (anonymous).
*
* This logic is a bit risky, for example in cases where the uid column
* - is not used for the user id (= overwriting unrelated values)
* - is not called "uid" (=missing related values)
* or
* but the risks are mitigated by limiting this to
* ContentEntityTypeInterface storage tables.
*
* @param \Drupal\user\UserInterface $user
* The user to reassign the content from.
*/
private function reassignContentOwnershipToAnonymous(UserInterface $user) {
$tables = $this->getTablesWithUidColumn();
if (empty($tables)) {
return;
}
// We don't care about the user entity:
unset($tables['user']);
// Init db object:
$database = $this->connection;
foreach ($tables as $table) {
if (isset($table['uid']) && !empty($table['uid'])) {
foreach ($table['uid'] as $table_name) {
$database->update($table_name)
->fields(['uid' => 0])
->condition('uid', $user->id(), '=')
->execute();
}
}
}
}
/**
* Retrieve all content entity tables which have a 'uid' column.
*
* @return array
* The table names.
*/
private function getTablesWithUidColumn() {
$entity_type_manager = $this->entityTypeManager;
$tables = [];
foreach ($entity_type_manager->getDefinitions() as $entity_type) {
// Only list content entity types using SQL storage:
if ($entity_type instanceof ContentEntityTypeInterface && in_array(SqlEntityStorageInterface::class, class_implements($entity_type->getStorageClass()))) {
$storage = $entity_type_manager->getStorage($entity_type->id());
$tables[$entity_type->id()]['uid'] = $storage->getTableMapping()->getAllFieldTableNames('uid');
}
}
return $tables;
}
}
......@@ -25,7 +25,7 @@ class NotLoggedDeleteAnonymizeTest extends SettingsBase {
$this->userStorage = $this->container->get('entity_type.manager')->getStorage('user');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
// Set the users for this scenario.
// Set the users for this scenario and create the node:
$this->addAdminUser();
$this->createTestUser();
......@@ -54,7 +54,7 @@ class NotLoggedDeleteAnonymizeTest extends SettingsBase {
}
/**
* Check the state of each user.
* Check the state of each user and their content.
*/
protected function checkTestResults(): void {
$account = $this->userStorage->load($this->admin->id());
......@@ -68,7 +68,7 @@ class NotLoggedDeleteAnonymizeTest extends SettingsBase {
$account = $this->userStorage->load($this->activeUserToDelete->id());
$this->assertNull($account);
// Confirm that user's content has been attributed to anonymous user.
// Check the users node ownership:
$test_node = $this->nodeStorage->loadUnchanged($this->node->id());
$this->assertTrue(($test_node->getOwnerId() == 0 && $test_node->isPublished()));
......@@ -92,6 +92,7 @@ class NotLoggedDeleteAnonymizeTest extends SettingsBase {
$this->activeUserToDelete = $this->createUser();
// Create a single node:
$this->node = $this->createNode([
'uid' => $this->activeUserToDelete->id(),
'published' => TRUE,
......
<?php
declare(strict_types = 1);
namespace Drupal\Tests\purge_users\Functional;
/**
* Purge users who did not log in for a specific period.
*
* - Tested with multiple created nodes to trigger the batch delete.
* - Purge method: delete the account and
* make its content belong to the Anonymous user.
* - Disregard inactive/blocked users unselected.
* - User Deletion Notification unselected.
*
* @group purge_users
*/
class NotLoggedDeleteAnonymizeTestBatchDelete extends SettingsBase {
/**
* Nodes created by a user.
*
* @var array
*/
protected $nodes;
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->userStorage = $this->container->get('entity_type.manager')->getStorage('user');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
// Set the users for this scenario.
$this->addAdminUser();
$this->createTestUser();
// Set the basic configuration and add the specific changes.
$this->setBasicConfig();
$this->config('purge_users.settings')
->set('user_lastlogin_value', '10')
->set('user_lastlogin_period', 'month')
->set('enabled_loggedin_users', TRUE)
->set('purge_user_cancel_method', 'user_cancel_reassign')
->save();
}
/**
* {@inheritdoc}
*/
protected function checkConfirmFormResults(): void {
$this->checkTestResults();
}
/**
* {@inheritdoc}
*/
protected function checkCronResults(): void {
$this->checkTestResults();
}
/**
* Check the state of each user and their content.
*/
protected function checkTestResults(): void {
$account = $this->userStorage->load($this->admin->id());
$this->assertNotNull($account);
// Never logged in user.
$account = $this->userStorage->load($this->neverLoggedUser->id());
$this->assertNotNull($account);
// Active user to be deleted.
$account = $this->userStorage->load($this->activeUserToDelete->id());
$this->assertNull($account);
// Check the users nodes ownership:
foreach ($this->nodes as $node) {
$test_node = $this->nodeStorage->loadUnchanged($node->id());
$this->assertTrue(($test_node->getOwnerId() == 0 && $test_node->isPublished()));
}
// Active user.
$account = $this->userStorage->load($this->activeUser->id());
$this->assertNotNull($account);
}
/**
* Active user settings.
*
* Expected to be deleted,
* their content anonymized.
*/
protected function createTestUser(): void {
// User is created 12 months ago and never logged in.
$this->neverLoggedUser = $this->createUser([], NULL, FALSE, [
'created' => strtotime('-12 month'),
'login' => 0,
]);
$this->activeUserToDelete = $this->createUser();
// Create more than 10 nodes, so the batch delete is triggered:
for ($i = 0; $i <= 11; $i++) {
$this->nodes[] = $this->createNode([
'uid' => $this->activeUserToDelete->id(),
'published' => TRUE,
]);
}
$this->activeUserToDelete->created = strtotime("-20 month");
$this->activeUserToDelete->login = strtotime("-13 month");
$this->activeUserToDelete->save();
// User is created 20 months ago and logged in 3 days ago.
$this->activeUser = $this->createUser([], NULL, FALSE, [
'created' => strtotime('-20 month'),
'login' => strtotime('-3 day'),
]);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment