purge_users-8.x-2.0/src/Services/UserManagementService.php
src/Services/UserManagementService.php
<?php
declare(strict_types=1);
namespace Drupal\purge_users\Services;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\UserInterface;
/**
* Class that holds the purging logic.
*
* @package Drupal\purge_users\Services
*/
class UserManagementService implements UserManagementServiceInterface {
use StringTranslationTrait;
/**
* Current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Logger channel service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* UserManagementService constructor.
*
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* Current user.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* 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, 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;
}
/**
* {@inheritdoc}
*
* @see \_user_cancel()
*/
public function purgeUser(UserInterface $user, string $method) {
$logger = $this->loggerFactory->get('purge_users');
$edit = [];
// When the 'user_cancel_delete' method is used, user_delete() is called,
// which invokes hook_ENTITY_TYPE_predelete() and hook_ENTITY_TYPE_delete()
// for the user entity. Modules should use those hooks to respond to the
// account deletion.
if ($method != 'user_cancel_delete') {
// Allow modules to add further sets to this batch.
/* @see \user_cancel() */
$this->moduleHandler->invokeAll('user_cancel', [$edit, $user, $method]);
}
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($this->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($this->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':
if ($user->isBlocked()) {
// The user is already blocked. Do not block them again.
return;
}
$this->notifyUserToPurge($user);
$user->block();
$user->save();
$this->messenger->addStatus($this->t('%name has been disabled.', ['%name' => $user->getDisplayName()]));
$logger->notice('Blocked user: %name %email.', [
'%name' => $user->getAccountName(),
'%email' => '<' . $user->getEmail() . '>',
]);
break;
default:
$logger->notice('Unknown user cancel method "%method".', [
'%method' => $method,
]);
break;
}
// After cancelling account, ensure that user is logged out. We can't
// destroy their session though, as we might have information in it, and we
// can't regenerate it because batch API uses the session ID, we will
// regenerate it in _user_cancel_session_regenerate().
if ($user->id() == $this->currentUser->id()) {
$this->currentUser->setAccount(new AnonymousUserSession());
}
}
/**
* Sends a notification to a user who is going to be purged now.
*
* @param \Drupal\user\UserInterface $user
* The user to be purged.
*/
protected function notifyUserToPurge(UserInterface $user): void {
// Send a notification email.
$config = $this->config->get('purge_users.settings');
if (!$config->get('send_email_notification')) {
return;
}
if ($this->userIsNotified($user->id(), 'purge_users')) {
// The user was already notified in the past. Don't do it again.
// @todo Review if this is the intended behavior.
return;
}
if (purge_users_send_notification_email($user)) {
$this->flagUserAsNotified($user->id(), 'purge_users');
}
}
/**
* {@inheritdoc}
*/
public function notifyUser(UserInterface $user): void {
if ($this->userIsNotified($user->id(), 'notification_users')) {
// The user was already notified in the past. Don't do it again.
return;
}
$notifier = $this->loggerFactory->get('notification_users');
if (purge_users_send_notification_email($user, 'notification_users')) {
$this->flagUserAsNotified($user->id(), 'notification_users');
$this->messenger->addStatus($this->t('%name has been notified.', ['%name' => $user->getDisplayName()]));
$notifier->notice('Notified user: %name %email.', [
'%name' => $user->getAccountName(),
'%email' => '<' . $user->getEmail() . '>',
]);
}
else {
$this->messenger->addStatus($this->t('User %name could not be notified. Check the logs.', ['%name' => $user->getDisplayName()]));
}
}
/**
* {@inheritdoc}
*/
public function userIsNotified(string $user_id, string $type): bool {
$notifier = $this->loggerFactory->get('notification_users');
$result = $this->connection->select('purge_users_notifications', 'n')
->fields('n', ['id'])
->condition('uid', $user_id)
->condition('type', $type)
->execute()
->fetch();
if (!empty($result)) {
$notifier->notice('User id %uid notification skipped.', [
'%uid' => $user_id,
]);
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function flagUserAsNotified(string $user_id, string $type): void {
$this->connection
->insert('purge_users_notifications')
->fields([
'uid' => $user_id,
'type' => $type,
'timestamp' => time(),
])
->execute();
}
/**
* {@inheritdoc}
*/
public function removeNotificationFlags(string $user_id): void {
$this->connection->delete('purge_users_notifications')
->condition('uid', $user_id)
->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;
}
}
