Problem/Motivation

When cancelling a user with the user_cancel_reassign method, its content is reassigned to the anonymous user. During this process in Drupal\node\NodeBulkUpdate::batchProcess, when encountering a trashed node, the process fails with the following error:

Update existing 'node' entity while changing the ID is not supported.

This is caused by loadUnchanged() returning NULL in EntityStorageBase::doPreSave():

// Load the original entity, if any.
if ($id_exists) {
  $entity->setOriginal($this->loadUnchanged($id));
}

The problem seems to be that loading a specific revision of the trashed node works:

\Drupal::entityTypeManager()->getStorage('node')->loadRevision(1525761)

But loading it by node ID doesn't:

\Drupal::entityTypeManager()->getStorage('node')->load(864766)

Proposed resolution

Issue fork trash-3578769

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

dieterholvoet created an issue. See original summary.

dieterholvoet’s picture

Issue summary: View changes
dieterholvoet’s picture

In my situation, I was able to work around the issue by wrapping all batch operations in a

<?php

namespace Drupal\mymodule\Plugin\QueueWorker;

use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Queue\Attribute\QueueWorker;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\plugin_constructor_factory_core\Plugin\QueueWorkerBase;
use Drupal\trash\TrashManagerInterface;

#[QueueWorker(
    id: self::ID,
    title: new TranslatableMarkup('Cancel a user account'),
    cron: ['time' => 60],
)]
class CancelUser extends QueueWorkerBase
{
    final public const ID = 'cancel_user';

    use DependencySerializationTrait;

    public function __construct(
        protected TrashManagerInterface $trashManager,
    ) {
    }

    public function processItem($data): void
    {
        user_cancel(
            edit: ['user_cancel_notify' => true, 'user_cancel_method' => 'user_cancel_reassign'],
            uid: $data,
            method: 'user_cancel_reassign',
        );

        // user_cancel() initiates a batch process. Run it manually.
        $batch = &batch_get();
        $batch['progressive'] = false;

        // Prevent issues updating trashed entities.
        foreach ($batch['sets'] as &$set) {
            foreach ($set['operations'] as &$operation) {
                [$callback, $arguments] = $operation;
                $operation[0] = [$this, 'executeInTrashContext'];
                $operation[1] = [$callback, $arguments];
            }
        }

        // Run the batch process immediately, since we're running in a queue worker and not a normal page request.
        if (function_exists('drush_backend_batch_process')) {
            drush_backend_batch_process();
        }
        else {
            batch_process();
        }
    }

    /**
     * Restores the previous trash context after processing the batch operations.
     */
    public function executeInTrashContext(mixed $callback, array $arguments, mixed $batchContext): void
    {
        $this->trashManager->executeInTrashContext('ignore', function () use ($callback, $arguments, $batchContext) {
            call_user_func_array($callback, array_merge($arguments, [&$batchContext]));
        });
    }
}
dave reid’s picture

I'm also running into this error while attempting to programmatically update an entity that is in the Trash (we're changing a filtered text field's text format from one value to another, so I want to ensure that even Trashed content is updated). Turns out I need to wrap the $entity->save() call in the trash context wrapper.

amateescu made their first commit to this issue’s fork.

amateescu’s picture

Status: Active » Needs review

This MR fixes the problem introduced by Trash, but cancelling an account with the "reassign content to anonymous" option now bumps into the exception from \Drupal\Core\Entity\ContentEntityStorageBase::doPreSave():

    if ($previously_default_revision && $no_longer_default && $original_same_as_current && $not_new_revision) {
      throw new EntityStorageException("An existing default revision of the '{$this->entityTypeId}' entity type can not be changed to a non-default revision.");
    }

That happens with non-trashed content though, so it needs to be a bug report for core.

  • amateescu committed a30df2f1 on 3.1.x
    feat: #3578769 Update existing 'node' entity while changing the ID is...
amateescu’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.