reassign_user_content-1.0.1/reassign_user_content.module
reassign_user_content.module
<?php
/**
* @file
* Reassign deleted user content to another user.
*/
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\group\Entity\GroupInterface;
use Drupal\reassign_user_content\MediaBatchService;
use Drupal\user\UserInterface;
/**
* Implements hook_help().
*/
function reassign_user_content_help($route_name, RouteMatchInterface $route_match) {
if ($route_name == 'help.page.reassign_user_content') {
return '<p>' . t('The Reassign User Content module allows you to reassign content of user you are about to delete to another user.') . '</p>';
}
}
/**
* Implements hook_form_alter().
*/
function reassign_user_content_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
$media_module_enabled = \Drupal::moduleHandler()->moduleExists('media');
$group_module_enabled = \Drupal::moduleHandler()->moduleExists('group');
if (in_array($form_id, [
'user_multiple_cancel_confirm',
'user_cancel_form',
])) {
$form['user_to_assign'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Choose user to assign content@media@group.', [
'@media' => $media_module_enabled ? ', media' : '',
'@group' => $group_module_enabled ? ', and groups' : '',
]),
'#target_type' => 'user',
'#states' => [
'visible' => [
[':input[name="user_cancel_method"]' => ['value' => 'user_cancel_reassign_content']],
],
'required' => [
[':input[name="user_cancel_method"]' => ['value' => 'user_cancel_reassign_content']],
],
],
];
$form['#validate'][] = 'reassign_user_content__cancel_user_form_validate';
}
}
/**
* User cancel Validate.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*/
function reassign_user_content__cancel_user_form_validate(array $form, FormStateInterface $form_state) : void {
$to_assign_uid = $form_state->getValue('user_to_assign');
// In case the user to assign is in the list of users to delete.
if (
$form['#form_id'] == 'user_multiple_cancel_confirm' &&
in_array($to_assign_uid, $form_state->getValue('accounts'))
) {
$form_state->setErrorByName('user_to_assign', t('Choose different user than the ones you want to delete.'));
}
else {
if (
$form_state->getValue('user_cancel_method') == 'user_cancel_reassign_content' &&
$form_state->getValue('user_to_assign') == $form_state->getValue('uid')
) {
$form_state->setErrorByName('user_to_assign', t('Choose different user than the deleted one to assign content.'));
}
}
}
/**
* Implements hook_user_cancel_methods_alter().
*/
function reassign_user_content_user_cancel_methods_alter(&$methods): void {
$media_module_enabled = \Drupal::moduleHandler()->moduleExists('media');
$group_module_enabled = \Drupal::moduleHandler()->moduleExists('group');
$methods['user_cancel_reassign_content'] = [
'title' => t('Delete the account and make its content@media@group belong to another user. This action cannot be undone.', [
'@media' => $media_module_enabled ? ', media' : '',
'@group' => $group_module_enabled ? ', and groups' : '',
]),
'description' => t('Your account will be removed and all account information deleted. All of your content will be assigned to the another user.'),
];
}
/**
* Implements hook_user_cancel().
*/
function reassign_user_content_user_cancel($edit, UserInterface $account, $method): void {
$user_to_assign_content = $edit['user_to_assign'] ?? NULL;
if ($method == 'user_cancel_reassign_content' && $user_to_assign_content) {
// Reassign nodes (current revisions).
\Drupal::moduleHandler()->loadInclude('node', 'inc', 'node.admin');
$nodes = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('uid', $account->id())
->execute();
node_mass_update($nodes, ['uid' => $user_to_assign_content], NULL, TRUE);
// Reassign old revisions.
\Drupal::database()->update('node_field_revision')
->fields(['uid' => $user_to_assign_content])
->condition('uid', $account->id())
->execute();
// Reassign old revisions.
\Drupal::database()->update('node_revision')
->fields(['revision_uid' => $user_to_assign_content])
->condition('revision_uid', $account->id())
->execute();
// Reassign revisions with moderated state (draft mode).
if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
\Drupal::database()->update('content_moderation_state_field_revision')
->fields(['uid' => $user_to_assign_content])
->condition('uid', $account->id())
->execute();
}
// Anonymize user comments.
if (\Drupal::moduleHandler()->moduleExists('comment')) {
$comments = \Drupal::entityTypeManager()
->getStorage('comment')
->loadByProperties(['uid' => $account->id()]);
_reassign_user_content_anonymize_comments($comments ?? []);
}
// Reassign user media if media module enabled.
if (\Drupal::moduleHandler()->moduleExists('media')) {
// Get all media of the deleted user.
$medias = \Drupal::entityTypeManager()
->getStorage('media')
->loadByProperties(['uid' => $account->id()]);
// Reassign all user medias using media batch.
\Drupal::service('class_resolver')
->getInstanceFromDefinition(MediaBatchService::class)
->reassignUserMedia($medias, $user_to_assign_content);
}
// Reassign user groups if group module enabled.
if (\Drupal::moduleHandler()->moduleExists('group')) {
$storage = \Drupal::entityTypeManager()->getStorage('group');
$gids = $storage->getQuery()
->accessCheck(FALSE)
->condition('uid', $account->id())
->execute();
// Run this as a batch if there are more than 10 groups.
if (count($gids) > 10) {
batch_set([
'operations' => [
[
'_reassign_user_content_group',
[$gids, $user_to_assign_content],
],
],
]);
}
else {
foreach ($storage->loadMultiple($gids) as $group) {
assert($group instanceof GroupInterface);
$group->set('uid', $user_to_assign_content);
$storage->save($group);
}
}
}
}
}
/**
* Implements hook_batch_alter().
*
* Alter _user_cancel batch operation
* because _user_cancel supports only the methods added by UserCancelForm.
*
* @see _user_cancel()
*/
function reassign_user_content_batch_alter(&$batch): void {
$values = isset($batch['form_state']) ? $batch['form_state']->getValues() : [];
// Only go over the batch sets if we come from the form-submit
// of the user cancel form, and our method was selected.
if (
$values &&
isset($values['user_cancel_method']) &&
$values['user_cancel_method'] === 'user_cancel_reassign_content'
) {
// For every account that gets deleted, the batch was created.
// Change all of them.
foreach ($batch['sets'] as &$set) {
if (
isset($set['operations'][0][0]) &&
$set['operations'][0][0] == '_user_cancel' &&
isset($set['operations'][0][1][2]) &&
$set['operations'][0][1][2] == 'user_cancel_reassign_content'
) {
// Change the batch operation callback.
$set['operations'][0][0] = 'reassign_user_content__reassign_user_content';
}
}
}
}
/**
* Implements callback_batch_operation().
*
* Similar to _user_cancel().
*
* @param array $edit
* An array of submitted form values.
* @param \Drupal\user\UserInterface $account
* The user ID of the user account to cancel.
* @param string $method
* The account cancellation method to use.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*
* @see _user_cancel()
*/
function reassign_user_content__reassign_user_content(array $edit, UserInterface $account, string $method): void {
$logger = \Drupal::logger('user');
if ($method == 'user_cancel_reassign_content') {
// Send account canceled notification if option was checked.
if (!empty($edit['user_cancel_notify'])) {
_user_mail_notify('status_canceled', $account);
}
$account->delete();
\Drupal::messenger()
->addStatus(t('Account %name has been deleted.', ['%name' => $account->getDisplayName()]));
$logger->notice('Deleted user: %name %email.', [
'%name' => $account->getAccountName(),
'%email' => '<' . $account->getEmail() . '>',
]);
}
// After cancelling an account, ensure that user is logged out.
// We can't destroy their session though, as we might have information on it,
// and we can't regenerate it because batch API uses the session ID,
// we will regenerate it in _user_cancel_session_regenerate().
if ($account->id() == \Drupal::currentUser()->id()) {
\Drupal::currentUser()->setAccount(new AnonymousUserSession());
}
}
/**
* Helper function to an anonymize array of comments.
*
* @param mixed $comments
* Comments array.
*/
function _reassign_user_content_anonymize_comments(array $comments): void {
if (empty($comments)) {
return;
}
if (count($comments) > 10) {
$batch_builder = (new BatchBuilder())
->addOperation('_reassign_user_content__anonymize_comments_batch_process', [$comments])
->setFinishCallback('_reassign_user_content__anonymize_comments_batch_finished')
->setTitle(t('Processing'))
->setErrorMessage(t('The update has encountered an error.'))
// We use a single multi-pass operation, so the default
// 'Remaining x of y operations' message will be confusing here.
->setProgressMessage('');
batch_set($batch_builder->toArray());
}
else {
_reassign_user_content_anonymize_chunk_comments($comments);
}
}
/**
* Helper function set anonymous as comment's owner.
*
* @param array $comments
* Array of comments to anonymize.
*/
function _reassign_user_content_anonymize_chunk_comments(array $comments): void {
foreach ($comments as $comment) {
$lang_codes = array_keys($comment->getTranslationLanguages());
// For efficiency, manually save the original comment before applying any
// changes.
$comment->original = clone $comment;
foreach ($lang_codes as $lang_code) {
$comment_translated = $comment->getTranslation($lang_code);
$comment_translated->setOwnerId(0);
$comment_translated->setAuthorName(
\Drupal::config('user.settings')->get('anonymous')
);
}
$comment->save();
}
}
/**
* Anonymize array comments batch process callback.
*
* @param array $comments
* Array of comments.
* @param mixed $context
* Batch context.
*/
function _reassign_user_content__anonymize_comments_batch_process(array $comments, &$context): void {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($comments);
$context['sandbox']['comments'] = $comments;
}
// Process comments by groups of 5.
$count = min(5, count($context['sandbox']['comments']));
for ($i = 1; $i <= $count; $i++) {
$comment = array_shift($context['sandbox']['comments']);
// For each comment, set the uid to anonymous, and save it.
_reassign_user_content_anonymize_chunk_comments([$comment]);
// Store result for post-processing in the finished callback.
$context['results'][] = Link::fromTextAndUrl($comment->label(), $comment->toUrl())->toString();
// Update our progress information.
$context['sandbox']['progress']++;
}
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
/**
* Anonymize comments batch finished callback.
*
* @param mixed $success
* Success.
* @param mixed $results
* Results.
* @param mixed $operations
* Operations.
*/
function _reassign_user_content__anonymize_comments_batch_finished($success, $results, $operations): void {
if ($success) {
\Drupal::messenger()
->addStatus(t('The comments update has been performed.'));
}
else {
\Drupal::messenger()
->addError(t('An error occurred and processing did not complete.'));
$message = \Drupal::translation()
->formatPlural(count($results), '1 item successfully processed:', '@count items successfully processed:');
$item_list = [
'#theme' => 'item_list',
'#items' => $results,
];
$message .= \Drupal::service('renderer')->render($item_list);
\Drupal::messenger()->addStatus($message);
}
}
/**
* Mass reassigns ownership of groups to given user.
*
* @param array $ids
* An array of group IDs.
* @param mixed $uid
* User to assign groups to.
* @param mixed $context
* Batch context.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _reassign_user_content_group(array $ids, $uid, &$context): void {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($ids);
$context['sandbox']['ids'] = $ids;
}
// Try to update 10 groups at a time.
$ids = array_slice($context['sandbox']['ids'], $context['sandbox']['progress'], 10);
$storage = \Drupal::entityTypeManager()->getStorage('group');
foreach ($storage->loadMultiple($ids) as $group) {
assert($group instanceof GroupInterface);
$group->set('uid', $uid);
$storage->save($group);
$context['sandbox']['progress']++;
}
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
