Problem/Motivation

At the moment every content entity type must implement hook_user_cancel() to do updates when a user is cancelled. We have comment_user_cancel() and node_user_cancel() but we are missing implementation for other Core Entities, including: taxonomy_user_cancel(), file_user_cancel() and media_user_cancel(). We need to handle both user_cancel_block_unpublish and user_cancel_reassign.

Also we have node_user_predelete() and comment_user_predelete() that will delete all the nodes and comments if the user is deleted. We might need something generic for that too.

In addition, many custom/contrib Content Entity Types likely have missing or incomplete implementations of the relevant hooks to properly react when a user is cancelled.

Proposed resolution

It has been decided to provide a generic solution using a Entity Handler dedicated to reacting on the cancelation a user.

The proposed handler will support each option that the administrator has for cancelling a user (mainly deleting all associated content or making that content anonymous). Additionally a implementation will also be provided which will support batch processing.

Each Content Entity Type will have to add this new handler to their Entity Type Annotation, developers can take advantage of the handlers provided by core, or can extend those handlers to define their own custom functionality. The latter is not required for custom Entity Types and the handlers provided will work with custom Entity Types.

Remaining tasks

  • Continue to refine the proposed resolution and review associated patches.
  • Framework manager decision is required on which namespace these new handler classes should use, see #39.

User interface changes

@todo - perhaps the user cancel form should give more indication of how many and what entities are going to be affected.

API changes

  1. Modifications to User module to provide new handlers.
  2. Modifications to some core modules to replace the usage of hook_user_cancel() with these new handlers.

Data model changes

No data model changes.

Release notes snippet

TODO.

CommentFileSizeAuthor
#97 3043725-97.patch42.16 KBJelle_S
#96 3043725-96.patch42.13 KBJelle_S
#89 interdiff_88_89.txt459 bytesameymudras
#89 3043725-89.patch42.17 KBameymudras
#88 interdiff_86_88.txt5.01 KBameymudras
#88 3043725-88.patch42.17 KBameymudras
#86 3043725-86.patch42.6 KBShubham Chandra
#85 3043725-85.patch11.46 KBShubham Chandra
#82 3043725-82.patch41.81 KBJelle_S
#80 3043725-80.patch41.77 KBJelle_S
#56 interdiff-3043725-55-56.txt691 bytesmohit_aghera
#56 3043725-56.patch25.63 KBmohit_aghera
#55 3043725-55.patch25.61 KBSuresh Prabhu Parkala
#50 3079085-user-cancellation-service-17.patch47.77 KBphenaproxima
#39 interdiff-3043725-36-39.txt5.81 KBphenaproxima
#39 3043725-39.patch25.58 KBphenaproxima
#36 3043725-36.patch25.37 KBphenaproxima
#34 3043725-34.patch25.33 KBphenaproxima
#31 3043725-31.patch25.67 KBphenaproxima
#28 interdiff-3043725-27-28.txt6.89 KBphenaproxima
#28 3043725-28.patch8.53 KBphenaproxima
#27 3043725-27.patch8.9 KBphenaproxima
#25 3043725-25.patch8.83 KBphenaproxima
#13 delete_user_owning_media_3043725-13-test-only.patch6.8 KBSpokje
#12 delete_user_owning_media_3043725-12-test-only.patch6.83 KBSpokje

Issue fork drupal-3043725

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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Title: So content entities are missing hook_user_cancel implementations » Some content entities are missing hook_user_cancel implementations
seanB’s picture

Having a generic solution sounds like a good plan, don't think the implementation needs to differ a lot between the entity types. As long as it's possible for entity types to override the default implementation, having a default would save developers a lot of work.

I can imagine you might also want different actions per entity type. For example, unpublish comments, reassign content and media. That might be more challenging.

alexpott’s picture

Issue summary: View changes

Ah worked out how the deletes occur - we have node_user_predelete() and comment_user_predelete() - we have to do something about that too.

AaronMcHale’s picture

Issue tags: +GDPR

Yes this sounds like a very good idea, it also aids in GDPR compliance.

Regarding the actual implementation, from the IS:

However this might cause more problems as something like a commerce order might implement EntityOwnerInterface and I'm not sure that deleting them is correct.

I agree with this, I see two possible ways forward:

  1. Opt-in per Entity Type: this could mean adding a new key to the Entity Type Annotation which when set to true would enable this, alternatively adding a new trait which must be added to each Entity class that wants to implement this. In the case of using a trait, the trait would contain the necessary code and functions which would be called on user cancel, this would require instanceof checks to be done before calling these methods on each Entity.
  2. Opt-out per Entity Type: this could simply involve a new key for the Entity Type Annotation (same as above) which is TRUE by default and for an Entity Type to opt-out they must add the key to their Entity Type Annotation and explicitly set it to FALSE.

One possible issue with the opt-out approach is that it could break backwards compatibility, so ultimately we might have to go with opt-in, update all Core Entity Types and communicate the change to Contrib through a change record and updated documentation.

andypost’s picture

Guess it could be event listener on entity, so every entity type of authored interface got reaction from opt-in/out

beram’s picture

I think this issue is starting to become critical no?

Since EntityOwnerTrait can be used as a default implementation of EntityOwnerInterface the owner can't be NULL anymore for media entities.

Therefore if you cancel a user you can't edit a media entity that was own by this user. The following error is thrown:

Drupal\Core\Entity\EntityStorageException: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "uid" violates not-null constraint DETAIL: Failing row contains (188, 241, image, nl, 1, building-buildings-busy-297743_0.jpg, 442, car, null, 1000, 667, null, 1558535344, 1563353147, 1, 1).: INSERT INTO drupal_webshop_nl.media_field_data (mid, vid, bundle, langcode, status, name, thumbnail__target_id, thumbnail__alt, thumbnail__title, thumbnail__width, thumbnail__height, uid, created, changed, default_langcode, revision_translation_affected) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, :db_insert_placeholder_8, :db_insert_placeholder_9, :db_insert_placeholder_10, :db_insert_placeholder_11, :db_insert_placeholder_12, :db_insert_placeholder_13, :db_insert_placeholder_14, :db_insert_placeholder_15); Array ( ) in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 847 of /var/www/html/src/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

(Captain Obvious Note: When updating the media entity if you add an owner no errors are thrown.)

beram’s picture

I opened the issue Quick win: Media entities support user cancel for people who need a temporary solution.

phenaproxima’s picture

Adding two related Media issues, which are probably smaller-scope duplicates of this one.

Spokje’s picture

Version: 8.8.x-dev » 8.7.x-dev

I've changed priority to Major because I think Cause a significant admin- or developer-facing bug with no workaround. applies here. For the same urgency reason changing this to Version 8.7.x-dev.

As shown in #3057004: Not having an author when editing/creating a Media Entity results in an EntityStorageException deleting a Media entities owning user and then trying to save one of these media entities leads to a Fatal Error and the Media entity can't be saved. Since Files and Taxonomies basically have the same issue, I expect the same thing to happen when deleting a user owning one of those entities.

As shown in #3069291: Quick win: Media entities support user cancel copying the "Node behaviour" for deleting a user fixes this issue for cases after the patch has been applied. For cases before the patch was applied a hook_update_N should be applied.

Now I'm all for solving this in a nice, generic way. But we also need something for now.

I therefore ask the Big Brains that wander this issue to advise on if we should be spending our time on fixing it in a (semi-) non-generic way, by applying the "Node behaviour" on Files, Media and Taxonomies in the way of #3069291 first to put out the immediate fire, and after that return to make it all pretty and nice, as it should be?
Or if we should direct our effort into getting this solved in the generic way straight away?

Spokje’s picture

Priority: Normal » Major

Right, changing "Priority to Major" he wrote, whilst not changing Priority to Major in the previous edit :/

Spokje’s picture

Here's a test that shows that deleting a user which owns a Media entity in anyway known to mankind me, so deleting programmatically through user_delete and through the GUI using both user_cancel_delete and user_cancel_reassign and then saving that Media entity through the GUI will end up in tears 500-errors.

This makes it impossible to save any Media entity, even unchanged when the owning user was correctly deleted.
I hope that proves my point for bumping this issue up to Major.

Spokje’s picture

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.9 was released on November 6 and is the final full bugfix release for the Drupal 8.7.x series. Drupal 8.7.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.8.0 on December 4, 2019. (Drupal 8.8.0-beta1 is available for testing.)

Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

dww’s picture

+100 to #9. Those two linked issues seem duplicates, but I'm not enough of a Media Maven to really
"decide" on the direction / scope here. This is the earliest issue, and the most general scope, and seems like the best place to focus efforts. But this is a major bug leading to real data loss in the wild, and the general solutions discussed here don't have patches yet. Therefore, it seems like leaving those other issues open is better for folks trying to work around this now. But, that risks fracturing the discussion, effort, etc. So perhaps linking those patches and adding the actual error messages to this summary (to make this issue more discoverable via search) would be best?

Meanwhile, #3057004-16: Not having an author when editing/creating a Media Entity results in an EntityStorageException and #3069291-2: Quick win: Media entities support user cancel are extremely similar patches, since both are basically just a copy/paste rename of node_mass_update() and friends to become media_mass_update(). That's already raising red flags about DRY ("Don't Repeat Yourself"). :(

Would anyone with more authority in such matters be willing to weigh in on how to best proceed? ;)

Thanks!
-Derek

phenaproxima’s picture

It seems like what would be really useful here is the ability to easily mass-update any content entities, not just nodes. There is already a pattern for this in core: the ConfigEntityUpdater class. What if we created something similar for content entities and then wrote thin wrappers for particular content entities? Something like:

function media_user_cancel($account) {
  Drupal::classResolver(ContentEntityUpdater::class)->update('media', function (MediaInterface $media) use ($account) {
    if ($media->getOwnerId() === $account->id()) {
      $media->setOwnerId(0);
      return TRUE;
    }
  });
}

Obviously we'd have to do some weird dancing around and through the Batch API, but it's worth it to prevent this widespread and incredibly unfortunate potential for data loss.

phenaproxima’s picture

Another option is to attack this from the other direction: we could make all core entities that implement EntityOwnerInterface insensitive to a missing user, rather than letting them get into fatal code paths. For example, at some point in its life cycle, the entity could verify that its owner exists, and take some sort of action on itself (like anonymizing and/or unpublishing) if not.

This solution feels more "magic" to me, though, so it wouldn't be my first choice. Just a thought.

larowlan’s picture

I feel like what we need here is something similar to what we do for moderation, have an entity 'handler' for user deleting.

User module would ship with a base class, and each entity-type could opt-in to this behaviour by adding a 'user_delete' handler.

Then user could implement a default hook_user_cancel and loop over entity types that have such a handler and do the necessary work.

Then node_user_cancel, comment_user_cancel etc could go away.

What are your thoughts on that sort of API @alexpott?

alexpott’s picture

I don't think we can do

we could make all core entities that implement EntityOwnerInterface insensitive to a missing user,

part of this is about privacy and choices about what data a site keeps on you when you remove an account.

Here's a list of the entity types in core that implement entity owner interface:

  • ContentModerationState - these will be deleted when the corresponding node is deleted - but the more interesting thing is what happens when the node owner is re-assigned - I'm not even sure why this has a UID. It seems wrong.
  • Comment - handled already - but it is amusing that the updates are not batched (better not delete myself from drupal.org)
  • Node - handled already
  • Workspace - needs something done
  • File - needs something done
  • Media - needs something done

Taxonomy terms are not owned. And neither are Shortcuts - though we need an issue to clean up the shortcut_set_users on user delete.

  • I think we should file an issue to discuss removing the owner stuff from ContentModerationState.
  • Files and Media are problematic - the problem is that these are designed to be shared - you are uploading to a media library. Therefore mass deleting all a user's media item feels odd. On the other hand - isn't that what a user would expect - the ability to remove everything they've ever uploaded.

Looking at some contrib examples:

  • Flag - deletes regardless of user cancel method - makes sense
  • Group - changes the cancel method descriptions a bit (concatenates t()! - can result in double escaping) - and reassigns to user 1 and not user 0
alexpott’s picture

I like the idea in #19 - making it a handler is a good idea. We could also trigger a deprecation if the entity type uses the entity owner trait and doesn't have the handler and in D10 throw an exception.

larowlan’s picture

Anyone interested in creating a proof of concept of #19?

Should we postpone the related issues on that?

AaronMcHale’s picture

Comment - handled already - but it is amusing that the updates are not batched (better not delete myself from drupal.org)

Literally laughed out loud when I read that, it's so true.

Files and Media are problematic - the problem is that these are designed to be shared - you are uploading to a media library. Therefore mass deleting all a user's media item feels odd. On the other hand - isn't that what a user would expect - the ability to remove everything they've ever uploaded.

Yes and no, I think it depends on the implementation, couple of scenarios:

  1. Content editors on a University website create nodes and media, if the user leaves the organisation their user is eventually deleted but those shared nodes and media items remain.
  2. Users of a community website create nodes, comments and media, they each have a "personal media library" (fancy way of saying modified Media Library View that only show content they've created), when a user is deleted you expect all of their content to be deleted, including what's in their personal media library.

I think scenario 2 is a bit of a stretch though, I imagine in reality everything would probably just become owned by the anonymous user to perverse the content. I'm sure there's a stringer example out there, but this was the best I could come up with in 5 minutes.

I also really like the idea proposed in #19, additionally I think it forces developers of custom entities (like me!) to think about what'll happen in this scenario, far too often I completely overlook this and it's almost a after thought, this solution puts it a bit more front end centre.

Anyone interested in creating a proof of concept of #19?

Should we postpone the related issues on that?

I'd love to, but I'm completely time pushed at the moment.

phenaproxima’s picture

Assigned: Unassigned » phenaproxima

I'll try a proof of concept by moving the node update stuff into a handler.

phenaproxima’s picture

Status: Active » Needs review
FileSize
8.83 KB

So here's a pretty crude proof of concept -- I took the stuff in the Node module and adapted it into an entity handler that should, in theory, work for any content entity type.

I don't know how much test coverage this has, but let's see what, if anything, it breaks. If it doesn't break anything, and people think this architecture looks good, I think the next step is probably to clean it up, refactor it into smaller, nicer methods, and add test coverage for all core entity types using this handler.

Status: Needs review » Needs work

The last submitted patch, 25: 3043725-25.patch, failed testing. View results

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
8.9 KB

Okay, this should fix the one broken test. Not bad for a proof of concept.

phenaproxima’s picture

Adopted BatchBuilder and simplified a couple of things. More refactoring is yet to come; the code we're adapting here hasn't been touched since 2014, and clearly it's from another era. I suspect we can make it a lot cleaner.

The last submitted patch, 27: 3043725-27.patch, failed testing. View results

andypost’s picture

phenaproxima’s picture

More refactoring. Since it's a big set of changes, no interdiff this time. Let's see if any tests break.

The most notable things here:

  • I may have missed something, but I don't think that the $revisions flag was needed. Given the way the entity query system purports to work, I see no reason we can't just always query for revisions. My guess, in this case, is that the code I adapted from node.admin.inc is utterly fossilized and not up-to-date with how the entity query system currently works.
  • Speaking of node.admin.inc, I removed it. It serves no further purpose, and node_mass_update(), despite not being internal, is not even executable without its helper methods -- all of which are prefixed with _ and therefore, as @larowlan tells it to me, not subject to our API contract.
  • Added an interface for the cancellation handler, with constants defining the built-in cancellation methods.
  • Split the implementation into two -- the base class, DefaultCancellationHandler, doesn't bother with the batch system. It is subclassed by BatchCancellationHandler, which does the Batch API stuff. I think this is quite a bit cleaner.
phenaproxima’s picture

Assigned: phenaproxima » Unassigned

I've done enough proving of the concept here, I think. De-assigning so others can tear into it!

phenaproxima’s picture

Speaking of node.admin.inc, I removed it. It serves no further purpose, and node_mass_update(), despite not being internal, is not even executable without its helper methods -- all of which are prefixed with _ and therefore, as @larowlan tells it to me, not subject to our API contract.

Idea for how to handle this from a BC perspective: we should probably just leave it exactly as it is, and deprecate the entire file for removal in Drupal 10. Although ultimately superseded by the cancellation handler, it does work rather differently -- it can mass update any field values, not just the ones related to ownership and publishing status. Which leads me to believe that there is very likely code out there which is using it for convenience. So deprecation seems like the least disruptive thing to do.

phenaproxima’s picture

Go big or go home.

Just to see how much stuff this breaks, I implemented user_user_cancel() to invoke all cancellation handlers, removing node_user_cancel() and comment_user_cancel() in the process. This also restores node.admin.inc based on my reasoning in #33. Per @alexpott's comment in #20, I also added the batch cancellation handler to media, workspaces, and files.

phenaproxima’s picture

Version: 8.9.x-dev » 9.1.x-dev

Argh! This should be a 9.1.x issue anyway.

phenaproxima’s picture

That'll teach me to be "clever" with core's dependency injection patterns.

Status: Needs review » Needs work

The last submitted patch, 36: 3043725-36.patch, failed testing. View results

AaronMcHale’s picture

I really like what's been done here so far, nice work @phenaproxima :)

Just a few minor things I picked up while reading through the patch:

  1. Could we have all of these classes sit in a sub-folder of each respective module, maybe: Handler, EntityHandler or even Entity\Handler? I'm just really mindful that many Core modules already have a lot of classes sitting at the top level under src. More from an organisational perspective, if we keep these in a sub-folder, it wouldn't contribute to the clutter and keeps things cleaner.
  2. +++ b/core/modules/comment/src/CancellationHandler.php
    @@ -0,0 +1,57 @@
    +<?php
    +
    +namespace Drupal\comment;
    +
    [...]
    +
    +class CancellationHandler extends DefaultCancellationHandler {
    

    Would it be better for Drupal\comment\CancellationHandler to extend BatchCancellationHandler instead of DefaultCancellationHandler, I imagine some sites will have users with thousands of comments, to quote @alexpott above:

    Comment - handled already - but it is amusing that the updates are not batched (better not delete myself from drupal.org)

  3. +/**
    + * Implements hook_user_cancel().
    + */
    +function user_user_cancel($edit, UserInterface $account, $method) {
    

    Is using hook_user_cancel() necessary? I mean, strictly speaking couldn't the code in here just be added to the existing user_cancel() function? To me it would seem simpler to do that.

Thanks,
-Aaron

phenaproxima’s picture

This should fix the test failures in #36.

Responding to Aaron's feedback:

  1. I agree they should be in a sub-folder, but not sure what it should be named. I'll leave that to the framework managers to make a recommendation, or point me to existing precedent in core. (For what it's worth, Content Moderation puts its handler in the Entity\Handler sub-namespace.)
  2. Ideally yes, but I think that might be best done in a follow-up issue, since it would be a behavior change.
  3. Right you are. Fixed.
phenaproxima’s picture

Status: Needs work » Needs review

Whoops.

AaronMcHale’s picture

Re #39

  1. Well in that case since Entity\Handler is already an existing pattern in a Core module, I'd vote to just continue that pattern, makes sense if you think about it, these are handler classes which relate directly to Entities. There is already a president in a number of areas for using these nested namespaces/folders, Plugin is the obvious one that comes to mind.
  2. Yep makes sense.
AaronMcHale’s picture

I had a further idea on this.

It would be nice if we could give administrators some level of visibility into which and how many Entities will be impacted by their chosen operation before they proceed.

Consider the scenario, a user who hadn't created any Nodes, but had authored many Media and Taxonomy entities, along with some other entities of custom types. This user is to be deleted, so I check the list of content for that user and nothing shows up, foolishly assuming that they only created some Nodes, then seeing they don't own any Nodes, I then assume its safe to delete that user, but only after realise how many Media, Taxonomy and other custom Entities have just been deleted. I now need to attempt to restore from some earlier backup of the site (this is assuming I have a backup).

I believe we could solve this by providing some visibility into which Entity Types and how many of those Entity Types will be impacted. We already have this kind of UI pattern in place for when you delete a Field, so I think using that similar pattern would be sensible.

In terms of the actual implementation this could and probably should be done in a follow-up issue, but I wanted to raise it here so that we could at least have visibility of the ground work that would be required. I envision we would probably want an additional method on the handler interface and base class. This method could be provided in the base class because all it would need to do is check that the entities of this type can be owned, and query for the number of entities with that owner, similar to the DefaultCancellationHandler::getQuery method in the latest path.

Happy to create the follow-up issue if we think it's a good idea to consider further.

Thanks,
-Aaron

AaronMcHale’s picture

Title: Some content entities are missing hook_user_cancel implementations » Provide a Entity Handler for user cancelation
Issue summary: View changes

Title and summary updates.

AaronMcHale’s picture

As per #42 and subsequent agreement in Slack, created #3163993: [PP-1] User Cancellation: give administrators visibility into which entities will be impacted by the cancellation of the particular user and postponed on this issue, set parent/child relationship.

andypost’s picture

andypost’s picture

phenaproxima credited dpi.

phenaproxima’s picture

Transferring credit from #3079085: Move user cancellation functions to a service and re-posting the latest patch from there, since it has some good work (not to mention tests) that we might want to merge into the current patch.

AaronMcHale’s picture

Re #50:

Yeah I like what's been accomplished there, merging that in seems sensible since both patches are very close to overlapping.

I like the idea of a service to essentially replace the user_cancel and related functions in user.module, what I wonder is, do we want to do anything about the actual delete method on the Entity Storage. Neither the User Entity Class nor the UserStorage storage class override EntityBase::delete nor SqlContentEntityStorage::delete. The reason I bring that up is because, in theory, a developer could simply call $user->delete() (which is basically what user_cancel is ultimately doing); but in doing so they completely bypass the user cancelation process, meaning Entities never get a chance to react to the deletion, and the result is a whole lot of broken Entities.

Now, let me clarify, this isn't a new problem, from what I can tell even in HEAD calling $user->delete() wouldn't even trigger hook_user_cancel() so this may be out of scope here and so could be dealt with in a follow-up, but figured it was worth mentioning.

Ultimately though that problem might end up being non-issue, because at the end of the day we are relying on calling $user->delete() in both our approach and in HEAD, and so we can't simply just override the delete methods and throw exceptions telling developers what the proper way to do it is, because logically that would also break our shiny new service.

Or would it... Well in theory we could override SqlContentEntityStorage::delete in UserStorage and just make it throw the exception I described, advising against using it, then in UserStorage we create a new method, called something like callParentDelete which just calls parent::delete, this method would be marked as @internal and not present in UserStorageInterface. In our service we then call UserStorage::callParentDelete, and in doing so we are still able to safely delete the User Entity, but avoid triggering the exception which would be triggered by calling UserStorage::delete.

Again though, happy to discuss more in a follow-up issue, and to avoid BC-break we could only warn against the use of User::delete and UserStorage::delete, in D10 we would be able to change this to thrown an exception.

andypost’s picture

Other option could be queue some jobs to process each kind of entity to run some default cancellation method.
So when user deleted from UI this jobs could run batch via each entity type having this handler

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Spokje’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

Patch #39 needs a reroll for 9.2.x.

Patch #50 also needs a reroll for 9.2.x.
Besides that, it needs to be merged with #39:

since it has some good work (not to mention tests) that we might want to merge into the current patch.

Thus spoketh phenaproxima in #39

Suresh Prabhu Parkala’s picture

Re-rolled #39 against latest 9.2.x.

mohit_aghera’s picture

Status: Needs work » Needs review
FileSize
25.63 KB
691 bytes

Attempt to fix the test cases. I tried to debug the root cause and found following things:

+++ b/core/modules/node/node.module
@@ -607,33 +606,6 @@ function node_ranking() {
-function node_user_cancel($edit, UserInterface $account, $method) {
-  switch ($method) {
-    case 'user_cancel_block_unpublish':
-      // Unpublish nodes (current revisions).
-      $nids = \Drupal::entityQuery('node')
-        ->accessCheck(FALSE)
-        ->condition('uid', $account->id())
-        ->execute();
-      module_load_include('inc', 'node', 'node.admin');
-      node_mass_update($nids, ['status' => 0], NULL, TRUE);
-      break;
-
-    case 'user_cancel_reassign':

If we see in old node_user_cancel() method, we are adding accessCheck(FALSe) to prevent additional acceess checks.
I think if we do the same thing, that might prevent the test case failures.

For now, I've added a patch with that fix. Let's see if it doesn't affect other test cases.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

AaronMcHale’s picture

Re #56 by @mohit_aghera: yeah I think that makes sense, if we were already doing that in the node module then makes sense to bring it over. It does seems rather unnecessary to trigger a bunch of access checking in this case.

AaronMcHale’s picture

Issue tags: +Needs followup

Just as a general reminder of outstanding follow-ups and discussion thus far:

We still have an outstanding discussion starting at #38 (#38.1) going through to #41 regarding what namespace this should live under. I'm in favour of Entity\Handler.

Similarly, in #38 (#38.2) through #41, we decided we might need a follow-up for the question:

Would it be better for Drupal\comment\CancellationHandler to extend BatchCancellationHandler instead of DefaultCancellationHandler

Personally, I don't mind creating this unless anyone else wants to beat me to it :)

In #51 I posed an idea about how we might address the implications of a developer simply calling $user->delete() without any prior checking, which could have pretty bad consequences in some situations. That change in itself could be considered a API breaking change, so I think that probably needs to be a follow-up so it can be properly discussed. Any opinions from anyone on whether it should be done here or in a follow-up?

phenaproxima’s picture

about how we might address the implications of a developer simply calling $user->delete() without any prior checking

I wonder if it might not make sense for the user storage handler to call the cancellation handler during delete(), unless there's some flag explicitly passed to bypass that? So basically, calling $user->delete() would invoke the cancellation stuff. If a developer wanted to delete a user specifically without cancellation, they'd have to do something like \Drupal::entityTypeManager()->getStorage('user')->delete($account, FALSE).

Thoughts?

AaronMcHale’s picture

I wonder if it might not make sense for the user storage handler to call the cancellation handler during delete(), unless there's some flag explicitly passed to bypass that? So basically, calling $user->delete() would invoke the cancellation stuff. If a developer wanted to delete a user specifically without cancellation, they'd have to do something like \Drupal::entityTypeManager()->getStorage('user')->delete($account, FALSE).

Actually, yeah that might be a good and rather elegant approach here.

Okay so, I think what we're basically saying is that, calling $user->delete() is the nice and safe way to do it, but as a developer if have a use-case where you really just want just get rid of that user from the database and either don't need to care or are (in other ways) dealing with the consequences, then go ahead and call ->getStorage('user')->delete($account, FALSE).

I like the logic with that, so I think that makes sense, and we probably should include code docs explaining that.

Do we want to somehow provide the option to pass the method that should be used e.g. CancellationHandlerInterface::METHOD_REASSIGN, and if so should there be a default option if none is passed (to satisfy EntityInterface::delete and EntityStorageInterface::delete?

phenaproxima’s picture

Do we want to somehow provide the option to pass the method that should be used e.g. CancellationHandlerInterface::METHOD_REASSIGN, and if so should there be a default option if none is passed (to satisfy EntityInterface::delete and EntityStorageInterface::delete?

IMHO, that is not something that should be exposed by User::delete() -- it should just use a hard-coded default cancellation method (probably whatever the default is now when a node is deleted). I'd think that, if you as a developer want to choose a specific cancellation method, you should invoke the cancellation handler directly (especially since, in such an advanced use case, you might want to use different cancellation methods for different cancellation-aware entity types).

AaronMcHale’s picture

IMHO, that is not something that should be exposed by User::delete() -- it should just use a hard-coded default cancellation method (probably whatever the default is now when a node is deleted). I'd think that, if you as a developer want to choose a specific cancellation method, you should invoke the cancellation handler directly (especially since, in such an advanced use case, you might want to use different cancellation methods for different cancellation-aware entity types).

Yeah that makes sense. I guess that then comes back round to my original thought, what should the default behaviour be?

Out of the four possible options, if you're calling ::delete, then you're not intending to just cancel/block the user (we have other methods if that is the intention e.g. UserInterface::block); So we're left with two choices: METHOD_REASSIGN or METHOD_DELETE. Given those two options, I say we should default to the lesser of two evils, METHOD_REASSIGN, as calling ::delete in my opinion shouldn't result in the indirect permanent deletion of potentially hundreds or thousands of other Entities, here we should do the minimal amount to ensure that deleting a user doesn't put a site in a broken state, that being triggering METHOD_REASSIGN.

In terms of implementing it, User::delete (as far as I can tell), isn't explicitly overridden, it just inherits the behaviour of EntityBase::delete, which ultimately just ends up calling SqlContentEntityStorage::delete, which in turn calls EntityStorageBase::delete. I imagine this is probably what we're going to end up overriding in any case, but there is a further layer of abstraction going on hidden behind abstract functions and hooks, so I didn't bother diving any deeper. We probably just need an override function in UserStorage and an implementation in UserStorageInterface for documentation and since we're introducing a new optional boolean parameter.

jonathanshaw’s picture

I wonder if it might not make sense for the user storage handler to call the cancellation handler during delete(), unless there's some flag explicitly passed to bypass that?

+1

============

If handlers are good for repsonding to user deletion, maybe they're good for respomdig to other deletions too. Would it not make sense to make the annotation more flexible to allow for this possibility?

The current patch proposes this annotation:

handlers = {
  ...
  "user_cancel" = "Drupal\user\BatchCancellationHandler",
}

invoked from user_cancel() like this:

  $entity_type_manager = \Drupal::entityTypeManager();
  foreach ($entity_type_manager->getDefinitions() as $entity_type) {
    if ($entity_type->hasHandlerClass('user_cancel')) {
      $entity_type_manager->getHandler($entity_type->id(), 'user_cancel')
        ->cancelAccount($account, $method);
    }
  }

Perhaps we should make this the annotation:

handlers = {
  ...
  "deletion" = {
    "user" = "Drupal\user\BatchCancellationHandler",
  }
}

and invoke it from EntityStorageBase like this:

public function delete(array $entities) {
  ...

  // Perform the delete and reset the static cache for the deleted entities.
  $this
    ->doDelete($keyed_entities);
  $this
    ->resetCache(array_keys($keyed_entities));

  foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
    $deletionHandlers = $entity_type->getHandlerClasses()['form']
    if (isset($deletionHandlers[$this->entityTypeId()]) {
      $handler = $this->entityTypeManager->createHandlerInstance($deletionHandlers[$this->entityTypeId()], $entity_type);
      $handler->delete($keyedEntities);
    }
  }

}

For minimal extra complexity we gain a significantly more flexible feature.

AaronMcHale’s picture

AaronMcHale’s picture

Re @jonathanshaw in #65: I like your thinking there, in theory it might work, thing is though we need to be able to pass the cancellation method to the handler, and your code snippet for EntityStorageBase doesn't seem to accommodate that. I'm not sure how we could accommodate that in a generic approach when the cancellation method is very specific to User Entities.

More broadly I think if another Entity type wanted to implement a deletion handler of sorts, they might also have some custom parameters that they want to have passed to the handler. Off the top of my head, I'm struggling to think of an example of another Entity Type that would want to provide a deletion/cancellation handler in as broad a sense as this.

As I said, I like that kind of thinking, I'm all for expanding the potential of the Entity API itself, but we would need to overcome those hurdles first I think.

phenaproxima’s picture

Category: Bug report » Feature request
Priority: Major » Normal

After poking at it a bit, I think we should defer the User::delete() stuff to a follow-up. Doing it here would bloat and confuse the scope of this issue. Adding entity type-specific handlers here is a clear feature request that doesn't change the existing behavior of User::delete(). Modifying the delete logic might mean backwards compatibility issues and (light) API changes, so it makes sense to handle those in another issue.

phenaproxima’s picture

Issue tags: -Needs reroll

The current merge request cleanly applies to 9.3.x, so removing the "Needs reroll" tag.

jonathanshaw’s picture

I think we should defer the User::delete() stuff to a follow-up. Doing it here would bloat and confuse the scope of this issue.

Fair enough.

I do still wonder whether it's be best to use a more generic annotation syntax that could be extended in future to entity types other than users. e.g.

handlers = {
  ...
  "deletion" = {
    "user" = "Drupal\user\BatchCancellationHandler",
  }
}
phenaproxima’s picture

I do still wonder whether it's be best to use a more generic annotation syntax that could be extended in future to entity types other than users. e.g.

To be honest, I don't really see how that is beneficial compared to hook_user_delete(), which AFAICT provides the same functionality...

jonathanshaw’s picture

I don't really see how that is beneficial compared to hook_user_delete(), which AFAICT provides the same functionality...

I don't really either, but the IS doesn't give me a clear idea of why a cancellation handler is beneficial compared with hook_user_handler. so I trusted there must be a reason and wondered if it applied more generically ... I don't the question to obstruct the work here though.

phenaproxima’s picture

Opened #3212623: [PP-1] Determine the relationship between deleting users and cancelling them to discuss how User::delete() and the cancellation handlers should relate and interact.

phenaproxima’s picture

Added #3212624: [PP-1] Comments should be batch processed upon user cancellation to convert comments to use the batch cancellation handler.

AaronMcHale’s picture

Issue tags: -Needs followup

Removing the Needs Followup tag that I added in #59 as all have now been created, thanks @phenaproxima for creating those, and yeah I'm happy to move the User::delete discussion to that followup :)

quietone’s picture

Status: Needs review » Needs work

jibran asked me to do a review.

I read the IS and am pleased to say it was concise, informative and quite up to date. Thank you alexpott and AaronMcHale. There were quite a few follows up to make and they have all been done. I found only 1 item that has not yet been action on. It is changing the namespace is " Use Entity\Handler namespace' This was brought up in #38, #39. In #41 it was pointed out that there was precedence so I think it should be implemented.

I then started to review the MR and managed to only do the files in modules/user before running out of time.

phenaproxima’s picture

Crediting @quietone for review.

quietone’s picture

Haha, and the joke is on me. jibran asked me to review a different issue!

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Jelle_S’s picture

Issue tags: +Needs reroll
FileSize
41.77 KB

This needs a reroll, but I don't have push access, so here's a patch for those who need it.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Jelle_S’s picture

FileSize
41.81 KB

Another reroll

rpayanm’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 82: 3043725-82.patch, failed testing. View results

Shubham Chandra’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
11.46 KB

Rerolled patch #80 with Drupal 9.5.x

Shubham Chandra’s picture

FileSize
42.6 KB

updated version of the patch of the #85 with Drupal 9.5.x

andypost’s picture

Status: Needs review » Needs work
+++ b/core/modules/node/tests/src/Functional/UserCancellationTest.php
@@ -0,0 +1,88 @@
+ * Tests how nodes react to user cancellation.

+++ b/core/modules/user/src/CancellationHandlerInterface.php
@@ -0,0 +1,64 @@
+ * Defines an interface for entity types to react to user account cancellation.
+ */
+interface CancellationHandlerInterface {

I think this handler needs better documentation,especially about when and how to add it

ameymudras’s picture

Fixed the coding standard issues related to the above patch #86. It still needs documentation changes so keeping it in needs work state

ameymudras’s picture

Fixed the coding standard issue with the above patch

joachim’s picture

  1. +++ b/core/modules/user/src/CancellationHandlerInterface.php
    @@ -0,0 +1,64 @@
    +   * With this cancellation method, the user account is blocked and the
    +   * current revision of any entity associated with the account is unpublished.
    

    These are saying what *should* be done by the handlers, so the wording 'is unpublished' doesn't seem right to me.

    Also, what should a handler do if its entity type doesn't support the published status?

  2. +++ b/core/modules/user/src/CancellationHandlerInterface.php
    @@ -0,0 +1,64 @@
    +   * revisions of any entity associated with it are reassigned to the anonymous
    

    Same - wording.

  3. +++ b/core/modules/user/src/CancellationHandlerInterface.php
    @@ -0,0 +1,64 @@
    +   * With this cancellation method, the user account is deleted. Modules may
    +   * implement entity hooks to react to the deletion as they see fit; it is
    +   * assumed that all entities associated with the account will be deleted.
    

    Why would they implement entity hooks -- isn't this what the handler is for?

  4. +++ b/core/modules/user/src/CancellationHandlerInterface.php
    @@ -0,0 +1,64 @@
    +   *   hook_user_cancel_methods().
    

    This is a new hook being invented. It needs to be documented.

  5. +++ b/core/modules/user/src/Entity/Handler/BatchCancellationHandler.php
    @@ -0,0 +1,216 @@
    +      batch_set($batch->toArray());
    

    What happens if a user entity is deleted via the API?

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

larowlan’s picture

Heads up there's another issue with a different approach over here #3135592: Cannot implement a custom user cancellation method - it would be a good idea to compare notes and try and agree on one approach that solves all issues.

claudiu.cristea’s picture

Just came across this, thanks to @larowlan. The main goal of #3135592: Cannot implement a custom user cancellation method is to solve an API bug, that prevents 3rd-party to implement a custom user cancellation method, even we pretend that it's possible. It's not. The modernization of the whole user cancellation process was only a necessity. The approach was suggested by @alexpott, in #3135592-14: Cannot implement a custom user cancellation method, who is also the author of this ticket.

I like the entity handler approach. However, I see some aspects:

  • The handler is dedicated to entity types, meaning that other 3rd party will still use hook_user_cancel(). Suddenly, we have two ways, two APIs, to react on user cancellation. #3135592: Cannot implement a custom user cancellation method is deprecating hook_user_cancel() (to be removed in D11) and puts in place a unique way to react on user cancellation, based on event dispatcher.
  • Dispatching an event might be a better API because you ca determine the order of 3rd-party reactions by using the subscribers priority. E.g. you want comment to react before node. Disclaimer: I didn't check yet if this solution allows that.
  • Didn't have time to read the code here but I wonder if this solves also #3135592: Cannot implement a custom user cancellation method

Should we reconsider all these architectural aspects here?

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

Jelle_S’s picture

FileSize
42.13 KB

Reroll of #89

Jelle_S’s picture

Status: Needs work » Needs review
FileSize
42.16 KB

Reroll of #96

claudiu.cristea’s picture

Status: Needs review » Needs work

This needs first a decision/discussion on #93