Prevent deletion of published content¶
Version v1
You can apply this model as a recipe (Drupal 10.3 or later) to your own Drupal site:
1 2 3 4 5 6 7 8 9 10 11 | |
Purpose¶
Published Article nodes cannot be deleted. To delete one, unpublish it first.
The model answers a single question — may this user delete this node? — by reacting to the entity access event and returning forbidden when the node is published. It does not touch any button, tab or link.
That distinction is the whole point. Drupal derives the Delete tab, the delete entry in the content list's operations dropdown, and the Delete button on the node edit form from the same entity access check. Answer the question once and all three disappear on their own, and the /node/N/delete route returns 403 for anyone who types it directly. You are not hiding three things; you are answering one question that three renderers happen to ask.
Testing it¶
Test as a user who is not user 1 and does not hold Bypass content access control.
Core short-circuits node access for the superuser and for anyone with that permission before hook_entity_access() runs, so ECA is never consulted. The failure is misleading in both directions: as user 1 you will conclude the model is broken, and if you only ever test as an editor you may believe content is protected when every administrator can still delete it.
What not to do¶
Three approaches look reasonable and are not:
- Reacting to the pre-delete event and throwing an exception. By the time that event fires, Drupal is already committed to the deletion. Only an exception stops it, and that leaves a broken request rather than a denied one.
- Hiding the delete button with form field access. It removes the button from the form and leaves
/node/N/deletefully reachable. The content is not protected, and the confirmation page still appears for anyone who navigates to it. - Removing the local task or the operations link. ECA can add and remove both, but doing it here suppresses the symptom on the surfaces you remember while the access answer still says allowed. Every surface you forget stays a working delete link.
Choosing the access result¶
The action defaults to forbidden, which is correct here: it is an absolute veto that no other access provider can override, and a protection rule that can be overridden is not protection.
The allow branch sets nothing at all. That is already neutral — ECA expresses no opinion and normal permissions decide. Set neutral explicitly only when you want the abstention to be visible in the diagram.
Do not use allowed in a model like this one. It is a positive grant that would hand delete rights to users who do not otherwise have them — the exact inverse of the intent. Other library models set allowed because they are opening something up; this one is the mirror image.
Order matters when several branches share one event: once access is denied it can never be granted again, so allow first and deny later.
Adapting it¶
The published check is only the predicate. Replace the condition and the same three-line structure enforces any rule you can express: forbid deleting a taxonomy term while content still references it, forbid deleting an order once it has been paid, forbid deleting a node outside an editorial window.
Keep entity_type_id, bundle and operation all set. Leaving operation empty matches every operation, which combined with the default result silently forbids viewing and editing too. ECA access results are also not cached, so narrow filters keep the cost down.
Dependencies¶
- module
- eca_access
- eca_content
- modeler_api