From a3ebad0633a785398ec039f38ec33cbb3056d098 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Sat, 17 Dec 2011 21:43:37 +0900
Subject: [PATCH] Issue #1346032 by xjm, aspilicious: Fixed Ordering of
 hook_entity_delete() is inconsistent.

---
 core/includes/file.inc                        |  12 +-
 core/modules/book/book.module                 |   4 +-
 core/modules/comment/comment.api.php          |  35 ++-
 core/modules/comment/comment.module           |  11 +-
 core/modules/entity/entity.api.php            |  38 ++-
 core/modules/entity/entity.controller.inc     |   7 +-
 .../entity/tests/entity_crud_hook_test.module |  53 ++++
 .../entity/tests/entity_crud_hook_test.test   | 240 +++++++++++-------
 core/modules/file/file.module                 |   4 +-
 core/modules/forum/forum.module               |   4 +-
 core/modules/image/image.module               |   4 +-
 core/modules/menu/menu.module                 |   4 +-
 core/modules/node/node.api.php                |  33 ++-
 core/modules/node/node.module                 |  20 +-
 .../node/tests/node_access_test.module        |   4 +-
 core/modules/path/path.module                 |   4 +-
 core/modules/poll/poll.module                 |   4 +-
 .../modules/simpletest/tests/file_test.module |   4 +-
 core/modules/statistics/statistics.module     |   8 +-
 core/modules/system/system.api.php            |  24 +-
 core/modules/taxonomy/taxonomy.api.php        |  58 ++++-
 core/modules/taxonomy/taxonomy.module         |  26 +-
 core/modules/tracker/tracker.module           |   4 +-
 core/modules/translation/translation.module   |   4 +-
 core/modules/trigger/trigger.module           |   8 +-
 core/modules/user/user.api.php                |  34 ++-
 core/modules/user/user.module                 |  21 +-
 27 files changed, 506 insertions(+), 166 deletions(-)

diff --git a/core/includes/file.inc b/core/includes/file.inc
index c4fca6df3f74..645c64b814f0 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -1225,6 +1225,7 @@ function file_create_filename($basename, $directory) {
  * @see file_unmanaged_delete()
  * @see file_usage_list()
  * @see file_usage_delete()
+ * @see hook_file_predelete()
  * @see hook_file_delete()
  */
 function file_delete(stdClass $file, $force = FALSE) {
@@ -1246,15 +1247,20 @@ function file_delete(stdClass $file, $force = FALSE) {
     return $references;
   }
 
-  // Let other modules clean up any references to the deleted file.
-  module_invoke_all('file_delete', $file);
-  module_invoke_all('entity_delete', $file, 'file');
+  // Let other modules clean up any references to the file prior to deletion.
+  module_invoke_all('file_predelete', $file);
+  module_invoke_all('entity_predelete', $file, 'file');
 
   // Make sure the file is deleted before removing its row from the
   // database, so UIs can still find the file in the database.
   if (file_unmanaged_delete($file->uri)) {
     db_delete('file_managed')->condition('fid', $file->fid)->execute();
     db_delete('file_usage')->condition('fid', $file->fid)->execute();
+
+    // Let other modules respond to file deletion.
+    module_invoke_all('file_delete', $file);
+    module_invoke_all('entity_delete', $file, 'file');
+
     return TRUE;
   }
   return FALSE;
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index ab57d38a44d7..5cda5e2513f1 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -959,9 +959,9 @@ function book_node_update($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function book_node_delete($node) {
+function book_node_predelete($node) {
   if (!empty($node->book['bid'])) {
     if ($node->nid == $node->book['bid']) {
       // Handle deletion of a top-level post.
diff --git a/core/modules/comment/comment.api.php b/core/modules/comment/comment.api.php
index 05912655b1e6..b381b6cfc4bd 100644
--- a/core/modules/comment/comment.api.php
+++ b/core/modules/comment/comment.api.php
@@ -129,12 +129,39 @@ function hook_comment_unpublish($comment) {
 }
 
 /**
- * The comment is being deleted by the moderator.
+ * Act before comment deletion.
+ *
+ * This hook is invoked from comment_delete_multiple() before
+ * field_attach_delete() is called and before the comment is actually removed
+ * from the database.
  *
  * @param $comment
- *   Passes in the comment the action is being performed on.
- * @return
- *   Nothing.
+ *   The comment object for the comment that is about to be deleted.
+ *
+ * @see hook_comment_delete()
+ * @see comment_delete_multiple()
+ * @see entity_delete_multiple()
+ */
+function hook_comment_predelete($comment) {
+  // Delete a record associated with the comment in a custom table.
+  db_delete('example_comment_table')
+    ->condition('cid', $comment->cid)
+    ->execute();
+}
+
+/**
+ * Respond to comment deletion.
+ *
+ * This hook is invoked from comment_delete_multiple() after
+ * field_attach_delete() has called and after the comment has been removed from
+ * the database.
+ *
+ * @param $comment
+ *   The comment object for the comment that has been deleted.
+ *
+ * @see hook_comment_predelete()
+ * @see comment_delete_multiple()
+ * @see entity_delete_multiple()
  */
 function hook_comment_delete($comment) {
   drupal_set_message(t('Comment: @subject has been deleted', array('@subject' => $comment->subject)));
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 77934999e8b5..e5e636b90ec4 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -1304,9 +1304,9 @@ function comment_node_insert($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function comment_node_delete($node) {
+function comment_node_predelete($node) {
   $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
   comment_delete_multiple($cids);
   db_delete('node_comment_statistics')
@@ -1402,9 +1402,9 @@ function comment_user_cancel($edit, $account, $method) {
 }
 
 /**
- * Implements hook_user_delete().
+ * Implements hook_user_predelete().
  */
-function comment_user_delete($account) {
+function comment_user_predelete($account) {
   $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
   comment_delete_multiple($cids);
 }
@@ -1457,6 +1457,9 @@ function comment_delete($cid) {
  *
  * @param $cids
  *   The comment to delete.
+ *
+ * @see hook_comment_predelete()
+ * @see hook_comment_delete()
  */
 function comment_delete_multiple($cids) {
   entity_delete_multiple('comment', $cids);
diff --git a/core/modules/entity/entity.api.php b/core/modules/entity/entity.api.php
index 9b19477163bf..e983778aa631 100644
--- a/core/modules/entity/entity.api.php
+++ b/core/modules/entity/entity.api.php
@@ -270,7 +270,7 @@ function hook_entity_insert($entity, $type) {
  * @param $entity
  *   The entity object.
  * @param $type
- *   The type of entity being updated (i.e. node, user, comment).
+ *   The type of entity being updated (e.g. node, user, comment).
  */
 function hook_entity_update($entity, $type) {
   // Update the entity's entry in a fictional table of all entities.
@@ -286,10 +286,42 @@ function hook_entity_update($entity, $type) {
 }
 
 /**
- * Act on entities when deleted.
+ * Act before entity deletion.
+ *
+ * This hook runs after the entity type-specific predelete hook.
  *
  * @param $entity
- *   The entity object.
+ *   The entity object for the entity that is about to be deleted.
+ * @param $type
+ *   The type of entity being deleted (e.g. node, user, comment).
+ */
+function hook_entity_predelete($entity, $type) {
+  // Count references to this entity in a custom table before they are removed
+  // upon entity deletion.
+  list($id) = entity_extract_ids($type, $entity);
+  $count = db_select('example_entity_data')
+    ->condition('type', $type)
+    ->condition('id', $id)
+    ->countQuery()
+    ->execute()
+    ->fetchField();
+
+  // Log the count in a table that records this statistic for deleted entities.
+  $ref_count_record = (object) array(
+    'count' => $count,
+    'type' => $type,
+    'id' => $id,
+  );
+  drupal_write_record('example_deleted_entity_statistics', $ref_count_record);
+}
+
+/**
+ * Respond to entity deletion.
+ *
+ * This hook runs after the entity type-specific delete hook.
+ *
+ * @param $entity
+ *   The entity object for the entity that has been deleted.
  * @param $type
  *   The type of entity being deleted (i.e. node, user, comment).
  */
diff --git a/core/modules/entity/entity.controller.inc b/core/modules/entity/entity.controller.inc
index cca18454fc1b..04c1f31a9796 100644
--- a/core/modules/entity/entity.controller.inc
+++ b/core/modules/entity/entity.controller.inc
@@ -454,6 +454,9 @@ public function delete($ids) {
 
     try {
       $this->preDelete($entities);
+      foreach ($entities as $id => $entity) {
+        $this->invokeHook('predelete', $entity);
+      }
       $ids = array_keys($entities);
 
       db_delete($this->entityInfo['base table'])
@@ -548,8 +551,8 @@ protected function postDelete($entities) { }
   /**
    * Invokes a hook on behalf of the entity.
    *
-   * @param $op
-   *   One of 'presave', 'insert', 'update', or 'delete'.
+   * @param $hook
+   *   One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
    * @param $entity
    *   The entity object.
    */
diff --git a/core/modules/entity/tests/entity_crud_hook_test.module b/core/modules/entity/tests/entity_crud_hook_test.module
index 873a162ced9c..435f7bf0cdc2 100644
--- a/core/modules/entity/tests/entity_crud_hook_test.module
+++ b/core/modules/entity/tests/entity_crud_hook_test.module
@@ -212,6 +212,59 @@ function entity_crud_hook_test_user_update() {
   $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
 }
 
+//
+// Predelete hooks
+//
+
+/**
+ * Implements hook_entity_predelete().
+ */
+function entity_crud_hook_test_entity_predelete($entity, $type) {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $type);
+}
+
+/**
+ * Implements hook_comment_predelete().
+ */
+function entity_crud_hook_test_comment_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
+/**
+ * Implements hook_file_predelete().
+ */
+function entity_crud_hook_test_file_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
+/**
+ * Implements hook_node_predelete().
+ */
+function entity_crud_hook_test_node_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
+/**
+ * Implements hook_taxonomy_term_predelete().
+ */
+function entity_crud_hook_test_taxonomy_term_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
+/**
+ * Implements hook_taxonomy_vocabulary_predelete().
+ */
+function entity_crud_hook_test_taxonomy_vocabulary_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
+/**
+ * Implements hook_user_predelete().
+ */
+function entity_crud_hook_test_user_predelete() {
+  $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
+}
+
 //
 // Delete hooks
 //
diff --git a/core/modules/entity/tests/entity_crud_hook_test.test b/core/modules/entity/tests/entity_crud_hook_test.test
index 782201ed1734..2bb459f66af6 100644
--- a/core/modules/entity/tests/entity_crud_hook_test.test
+++ b/core/modules/entity/tests/entity_crud_hook_test.test
@@ -6,6 +6,7 @@
  * - hook_entity_insert()
  * - hook_entity_load()
  * - hook_entity_update()
+ * - hook_entity_predelete()
  * - hook_entity_delete()
  * As well as all type-specific hooks, like hook_node_insert(),
  * hook_comment_update(), etc.
@@ -27,24 +28,28 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
   }
 
   /**
-   * Pass if the message $text was set by one of the CRUD hooks in
-   * entity_crud_hook_test.module, i.e., if the $text is an element of
-   * $_SESSION['entity_crud_hook_test'].
+   * Checks the order of CRUD hook execution messages.
    *
-   * @param $text
-   *   Plain text to look for.
-   * @param $message
-   *   Message to display.
-   * @param $group
-   *   The group this message belongs to, defaults to 'Other'.
-   * @return
-   *   TRUE on pass, FALSE on fail.
+   * entity_crud_hook_test.module implements all core entity CRUD hooks and
+   * stores a message for each in $_SESSION['entity_crud_hook_test'].
+   *
+   * @param $messages
+   *   An array of plain-text messages in the order they should appear.
    */
-  protected function assertHookMessage($text, $message = NULL, $group = 'Other') {
-    if (!isset($message)) {
-      $message = $text;
+  protected function assertHookMessageOrder($messages) {
+    $positions = array();
+    foreach ($messages as $message) {
+      // Verify that each message is found and record its position.
+      $position = array_search($message, $_SESSION['entity_crud_hook_test']);
+      if ($this->assertTrue($position !== FALSE, $message)) {
+        $positions[] = $position;
+      }
     }
-    return $this->assertTrue(array_search($text, $_SESSION['entity_crud_hook_test']) !== FALSE, $message, $group);
+
+    // Sort the positions and ensure they remain in the same order.
+    $sorted = $positions;
+    sort($sorted);
+    $this->assertTrue($sorted == $positions, 'The hook messages appear in the correct order.');
   }
 
   /**
@@ -77,34 +82,45 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
       'status' => 1,
       'language' => LANGUAGE_NONE,
     ));
+
     $_SESSION['entity_crud_hook_test'] = array();
     comment_save($comment);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_comment_presave called',
+      'entity_crud_hook_test_entity_presave called for type comment',
+      'entity_crud_hook_test_comment_insert called',
+      'entity_crud_hook_test_entity_insert called for type comment',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $comment = comment_load($comment->cid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type comment',
+      'entity_crud_hook_test_comment_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $comment->subject = 'New subject';
     comment_save($comment);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_comment_presave called',
+      'entity_crud_hook_test_entity_presave called for type comment',
+      'entity_crud_hook_test_comment_update called',
+      'entity_crud_hook_test_entity_update called for type comment',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     comment_delete($comment->cid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type comment');
-    $this->assertHookMessage('entity_crud_hook_test_comment_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_comment_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type comment',
+      'entity_crud_hook_test_comment_delete called',
+      'entity_crud_hook_test_entity_delete called for type comment',
+    ));
   }
 
   /**
@@ -126,31 +142,41 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     $_SESSION['entity_crud_hook_test'] = array();
     file_save($file);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_file_presave called',
+      'entity_crud_hook_test_entity_presave called for type file',
+      'entity_crud_hook_test_file_insert called',
+      'entity_crud_hook_test_entity_insert called for type file',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $file = file_load($file->fid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type file',
+      'entity_crud_hook_test_file_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $file->filename = 'new.entity_crud_hook_test.file';
     file_save($file);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_file_presave called',
+      'entity_crud_hook_test_entity_presave called for type file',
+      'entity_crud_hook_test_file_update called',
+      'entity_crud_hook_test_entity_update called for type file',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     file_delete($file);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type file');
-    $this->assertHookMessage('entity_crud_hook_test_file_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_file_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type file',
+      'entity_crud_hook_test_file_delete called',
+      'entity_crud_hook_test_entity_delete called for type file',
+    ));
   }
 
   /**
@@ -172,31 +198,41 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     $_SESSION['entity_crud_hook_test'] = array();
     node_save($node);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_node_presave called',
+      'entity_crud_hook_test_entity_presave called for type node',
+      'entity_crud_hook_test_node_insert called',
+      'entity_crud_hook_test_entity_insert called for type node',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $node = node_load($node->nid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type node',
+      'entity_crud_hook_test_node_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $node->title = 'New title';
     node_save($node);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_node_presave called',
+      'entity_crud_hook_test_entity_presave called for type node',
+      'entity_crud_hook_test_node_update called',
+      'entity_crud_hook_test_entity_update called for type node',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     node_delete($node->nid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type node');
-    $this->assertHookMessage('entity_crud_hook_test_node_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_node_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type node',
+      'entity_crud_hook_test_node_delete called',
+      'entity_crud_hook_test_entity_delete called for type node',
+    ));
   }
 
   /**
@@ -220,31 +256,41 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     $_SESSION['entity_crud_hook_test'] = array();
     taxonomy_term_save($term);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_term_presave called',
+      'entity_crud_hook_test_entity_presave called for type taxonomy_term',
+      'entity_crud_hook_test_taxonomy_term_insert called',
+      'entity_crud_hook_test_entity_insert called for type taxonomy_term',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $term = taxonomy_term_load($term->tid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type taxonomy_term',
+      'entity_crud_hook_test_taxonomy_term_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $term->name = 'New name';
     taxonomy_term_save($term);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_term_presave called',
+      'entity_crud_hook_test_entity_presave called for type taxonomy_term',
+      'entity_crud_hook_test_taxonomy_term_update called',
+      'entity_crud_hook_test_entity_update called for type taxonomy_term',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     taxonomy_term_delete($term->tid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type taxonomy_term');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_term_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_term_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type taxonomy_term',
+      'entity_crud_hook_test_taxonomy_term_delete called',
+      'entity_crud_hook_test_entity_delete called for type taxonomy_term',
+    ));
   }
 
   /**
@@ -260,31 +306,41 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     $_SESSION['entity_crud_hook_test'] = array();
     taxonomy_vocabulary_save($vocabulary);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_vocabulary_presave called',
+      'entity_crud_hook_test_entity_presave called for type taxonomy_vocabulary',
+      'entity_crud_hook_test_taxonomy_vocabulary_insert called',
+      'entity_crud_hook_test_entity_insert called for type taxonomy_vocabulary',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $vocabulary = taxonomy_vocabulary_load($vocabulary->vid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type taxonomy_vocabulary',
+      'entity_crud_hook_test_taxonomy_vocabulary_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $vocabulary->name = 'New name';
     taxonomy_vocabulary_save($vocabulary);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_vocabulary_presave called',
+      'entity_crud_hook_test_entity_presave called for type taxonomy_vocabulary',
+      'entity_crud_hook_test_taxonomy_vocabulary_update called',
+      'entity_crud_hook_test_entity_update called for type taxonomy_vocabulary',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     taxonomy_vocabulary_delete($vocabulary->vid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type taxonomy_vocabulary');
-    $this->assertHookMessage('entity_crud_hook_test_taxonomy_vocabulary_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_taxonomy_vocabulary_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type taxonomy_vocabulary',
+      'entity_crud_hook_test_taxonomy_vocabulary_delete called',
+      'entity_crud_hook_test_entity_delete called for type taxonomy_vocabulary',
+    ));
   }
 
   /**
@@ -302,31 +358,41 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     $_SESSION['entity_crud_hook_test'] = array();
     $account = user_save($account, $edit);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_insert called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_insert called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_user_presave called',
+      'entity_crud_hook_test_entity_presave called for type user',
+      'entity_crud_hook_test_user_insert called',
+      'entity_crud_hook_test_entity_insert called for type user',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $account = user_load($account->uid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_load called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_load called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_entity_load called for type user',
+      'entity_crud_hook_test_user_load called',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     $edit['name'] = 'New name';
     $account = user_save($account, $edit);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_presave called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_presave called');
-    $this->assertHookMessage('entity_crud_hook_test_entity_update called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_update called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_user_presave called',
+      'entity_crud_hook_test_entity_presave called for type user',
+      'entity_crud_hook_test_user_update called',
+      'entity_crud_hook_test_entity_update called for type user',
+    ));
 
     $_SESSION['entity_crud_hook_test'] = array();
     user_delete($account->uid);
 
-    $this->assertHookMessage('entity_crud_hook_test_entity_delete called for type user');
-    $this->assertHookMessage('entity_crud_hook_test_user_delete called');
+    $this->assertHookMessageOrder(array(
+      'entity_crud_hook_test_user_predelete called',
+      'entity_crud_hook_test_entity_predelete called for type user',
+      'entity_crud_hook_test_user_delete called',
+      'entity_crud_hook_test_entity_delete called for type user',
+    ));
   }
 
 }
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index f5938a8728f5..68204aa7a5ff 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -347,9 +347,9 @@ function file_progress_implementation() {
 }
 
 /**
- * Implements hook_file_delete().
+ * Implements hook_file_predelete().
  */
-function file_file_delete($file) {
+function file_file_predelete($file) {
   // TODO: Remove references to a file that is in-use.
 }
 
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 3575bfd4edbb..26ab78f681e9 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -404,9 +404,9 @@ function forum_node_insert($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function forum_node_delete($node) {
+function forum_node_predelete($node) {
   if (_forum_node_check_node_type($node)) {
     db_delete('forum')
       ->condition('nid', $node->nid)
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 3d6453f51649..f577b76e0170 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -323,9 +323,9 @@ function image_file_move($file, $source) {
 }
 
 /**
- * Implements hook_file_delete().
+ * Implements hook_file_predelete().
  */
-function image_file_delete($file) {
+function image_file_predelete($file) {
   // Delete any image derivatives of this image.
   image_path_flush($file->uri);
 }
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index b10a17adfa99..1840cec84ea1 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -550,9 +550,9 @@ function menu_node_save($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function menu_node_delete($node) {
+function menu_node_predelete($node) {
   // Delete all menu module links that point to this node.
   $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/' . $node->nid), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $m) {
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index d526c484f30f..60d178989871 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -88,10 +88,12 @@
  * - Deleting a node (calling node_delete() or node_delete_multiple()):
  *   - Node is loaded (see Loading section above)
  *   - hook_delete() (node-type-specific)
- *   - hook_node_delete() (all)
- *   - hook_entity_delete() (all)
+ *   - hook_node_predelete() (all)
+ *   - hook_entity_predelete() (all)
  *   - field_attach_delete()
  *   - Node and revision information are deleted from database
+ *   - hook_node_delete() (all)
+ *   - hook_entity_delete() (all)
  * - Deleting a node revision (calling node_revision_delete()):
  *   - Node is loaded (see Loading section above)
  *   - Revision information is deleted from database
@@ -447,24 +449,43 @@ function hook_node_operations() {
 }
 
 /**
- * Respond to node deletion.
+ * Act before node deletion.
  *
  * This hook is invoked from node_delete_multiple() after the type-specific
- * hook_delete() has been invoked, but before hook_entity_delete and
+ * hook_delete() has been invoked, but before hook_entity_predelete() and
  * field_attach_delete() are called, and before the node is removed from the
  * node table in the database.
  *
  * @param $node
- *   The node that is being deleted.
+ *   The node that is about to be deleted.
  *
+ * @see hook_node_predelete()
+ * @see node_delete_multiple()
  * @ingroup node_api_hooks
  */
-function hook_node_delete($node) {
+function hook_node_predelete($node) {
   db_delete('mytable')
     ->condition('nid', $node->nid)
     ->execute();
 }
 
+/**
+ * Respond to node deletion.
+ *
+ * This hook is invoked from node_delete_multiple() after field_attach_delete()
+ * has been called and after the node has been removed from the database.
+ *
+ * @param $node
+ *   The node that has been deleted.
+ *
+ * @see hook_node_predelete()
+ * @see node_delete_multiple()
+ * @ingroup node_api_hooks
+ */
+function hook_node_delete($node) {
+  drupal_set_message(t('Node: @title has been deleted', array('@title' => $node->title)));
+}
+
 /**
  * Respond to deletion of a node revision.
  *
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 7c2566607333..2a6aaa347206 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -1221,6 +1221,9 @@ function node_delete($nid) {
  *
  * @param $nids
  *   An array of node IDs.
+ *
+ * @see hook_node_predelete()
+ * @see hook_node_delete()
  */
 function node_delete_multiple($nids) {
   $transaction = db_transaction();
@@ -1231,8 +1234,11 @@ function node_delete_multiple($nids) {
       foreach ($nodes as $nid => $node) {
         // Call the node-specific callback (if any):
         node_invoke($node, 'delete');
-        module_invoke_all('node_delete', $node);
-        module_invoke_all('entity_delete', $node, 'node');
+
+        // Allow modules to act prior to node deletion.
+        module_invoke_all('node_predelete', $node);
+        module_invoke_all('entity_predelete', $node, 'node');
+
         field_attach_delete('node', $node);
 
         // Remove this node from the search index if needed.
@@ -1257,6 +1263,12 @@ function node_delete_multiple($nids) {
       db_delete('node_access')
        ->condition('nid', $nids, 'IN')
        ->execute();
+
+      foreach ($nodes as $nid => $node) {
+        // Allow modules to respond to node deletion.
+        module_invoke_all('node_delete', $node);
+        module_invoke_all('entity_delete', $node, 'node');
+      }
     }
     catch (Exception $e) {
       $transaction->rollback();
@@ -1815,9 +1827,9 @@ function node_user_cancel($edit, $account, $method) {
 }
 
 /**
- * Implements hook_user_delete().
+ * Implements hook_user_predelete().
  */
-function node_user_delete($account) {
+function node_user_predelete($account) {
   // Delete nodes (current revisions).
   // @todo Introduce node_mass_delete() or make node_mass_update() more flexible.
   $nodes = db_select('node', 'n')
diff --git a/core/modules/node/tests/node_access_test.module b/core/modules/node/tests/node_access_test.module
index 34d223708510..f946573d6416 100644
--- a/core/modules/node/tests/node_access_test.module
+++ b/core/modules/node/tests/node_access_test.module
@@ -192,10 +192,10 @@ function node_access_test_node_load($nodes, $types) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
 
-function node_access_test_node_delete($node) {
+function node_access_test_node_predelete($node) {
   db_delete('node_access_test')->condition('nid', $node->nid)->execute();
 }
 
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index df1193666bba..563bfc717d55 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -217,9 +217,9 @@ function path_node_update($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function path_node_delete($node) {
+function path_node_predelete($node) {
   // Delete all aliases associated with this node.
   path_delete(array('source' => 'node/' . $node->nid));
 }
diff --git a/core/modules/poll/poll.module b/core/modules/poll/poll.module
index ec5452e34ebe..7fe34e412ec8 100644
--- a/core/modules/poll/poll.module
+++ b/core/modules/poll/poll.module
@@ -986,9 +986,9 @@ function poll_user_cancel($edit, $account, $method) {
 }
 
 /**
- * Implements hook_user_delete().
+ * Implements hook_user_predelete().
  */
-function poll_user_delete($account) {
+function poll_user_predelete($account) {
   db_delete('poll_vote')
     ->condition('uid', $account->uid)
     ->execute();
diff --git a/core/modules/simpletest/tests/file_test.module b/core/modules/simpletest/tests/file_test.module
index bfeee56b2230..21c741902214 100644
--- a/core/modules/simpletest/tests/file_test.module
+++ b/core/modules/simpletest/tests/file_test.module
@@ -303,9 +303,9 @@ function file_test_file_move($file, $source) {
 }
 
 /**
- * Implements hook_file_delete().
+ * Implements hook_file_predelete().
  */
-function file_test_file_delete($file) {
+function file_test_file_predelete($file) {
   _file_test_log_call('delete', array($file));
 }
 
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index 4f725530365b..191d82bdbd9d 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -218,9 +218,9 @@ function statistics_user_cancel($edit, $account, $method) {
 }
 
 /**
- * Implements hook_user_delete().
+ * Implements hook_user_predelete().
  */
-function statistics_user_delete($account) {
+function statistics_user_predelete($account) {
   db_delete('accesslog')
     ->condition('uid', $account->uid)
     ->execute();
@@ -391,9 +391,9 @@ function _statistics_format_item($title, $path) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function statistics_node_delete($node) {
+function statistics_node_predelete($node) {
   // clean up statistics table when node is deleted
   db_delete('node_counter')
     ->condition('nid', $node->nid)
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 548622ccb837..872b002ca375 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -2394,11 +2394,33 @@ function hook_file_move($file, $source) {
 }
 
 /**
- * Respond to a file being deleted.
+ * Act prior to file deletion.
+ *
+ * This hook is invoked from file_delete() before the file is removed from the
+ * filesystem and before its records are removed from the database.
+ *
+ * @param $file
+ *   The file that is about to be deleted.
+ *
+ * @see hook_file_delete()
+ * @see file_delete()
+ * @see upload_file_delete()
+ */
+function hook_file_predelete($file) {
+  // Delete all information associated with the file.
+  db_delete('upload')->condition('fid', $file->fid)->execute();
+}
+
+/**
+ * Respond to file deletion.
+ *
+ * This hook is invoked from file_delete() after the file has been removed from
+ * the filesystem and after its records have been removed from the database.
  *
  * @param $file
  *   The file that has just been deleted.
  *
+ * @see hook_file_predelete()
  * @see file_delete()
  */
 function hook_file_delete($file) {
diff --git a/core/modules/taxonomy/taxonomy.api.php b/core/modules/taxonomy/taxonomy.api.php
index cb778c9a764f..426306d6c77b 100644
--- a/core/modules/taxonomy/taxonomy.api.php
+++ b/core/modules/taxonomy/taxonomy.api.php
@@ -70,13 +70,37 @@ function hook_taxonomy_vocabulary_update($vocabulary) {
 }
 
 /**
- * Respond to the deletion of taxonomy vocabularies.
+ * Act before taxonomy vocabulary deletion.
  *
- * Modules implementing this hook can respond to the deletion of taxonomy
- * vocabularies from the database.
+ * This hook is invoked from taxonomy_vocabulary_delete() before
+ * field_attach_delete_bundle() is called and before the vocabulary is actually
+ * removed from the database.
  *
  * @param $vocabulary
- *   A taxonomy vocabulary object.
+ *   The taxonomy vocabulary object for the vocabulary that is about to be
+ *   deleted.
+ *
+ * @see hook_taxonomy_vocabulary_delete()
+ * @see taxonomy_vocabulary_delete()
+ */
+function hook_taxonomy_vocabulary_predelete($vocabulary) {
+  if (variable_get('taxonomy_' . $vocabulary->vid . '_synonyms', FALSE)) {
+    variable_del('taxonomy_' . $vocabulary->vid . '_synonyms');
+  }
+}
+
+/**
+ * Respond to taxonomy vocabulary deletion.
+ *
+ * This hook is invoked from taxonomy_vocabulary_delete() after
+ * field_attach_delete_bundle() has been called and after the vocabulary has
+ * been removed from the database.
+ *
+ * @param $vocabulary
+ *   The taxonomy vocabulary object for the vocabulary that has been deleted.
+ *
+ * @see hook_taxonomy_vocabulary_predelete()
+ * @see taxonomy_vocabulary_delete()
  */
 function hook_taxonomy_vocabulary_delete($vocabulary) {
   if (variable_get('taxonomy_' . $vocabulary->vid . '_synonyms', FALSE)) {
@@ -169,13 +193,31 @@ function hook_taxonomy_term_update($term) {
 }
 
 /**
- * Respond to the deletion of taxonomy terms.
+ * Act before taxonomy term deletion.
  *
- * Modules implementing this hook can respond to the deletion of taxonomy
- * terms from the database.
+ * This hook is invoked from taxonomy_term_delete() before
+ * field_attach_delete() is called and before the term is actually removed from
+ * the database.
  *
  * @param $term
- *   A taxonomy term object.
+ *   The taxonomy term object for the term that is about to be deleted.
+ *
+ * @see taxonomy_term_delete()
+ */
+function hook_taxonomy_term_predelete($term) {
+  db_delete('term_synoynm')->condition('tid', $term->tid)->execute();
+}
+
+/**
+ * Respond to taxonomy term deletion.
+ *
+ * This hook is invoked from taxonomy_term_delete() after field_attach_delete()
+ * has been called and after the term has been removed from the database.
+ *
+ * @param $term
+ *   The taxonomy term object for the term that has been deleted.
+ *
+ * @see taxonomy_term_delete()
  */
 function hook_taxonomy_term_delete($term) {
   db_delete('term_synoynm')->condition('tid', $term->tid)->execute();
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 4eadf5a22de7..191b51972fd5 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -449,12 +449,19 @@ function taxonomy_vocabulary_save($vocabulary) {
  *   A vocabulary ID.
  * @return
  *   Constant indicating items were deleted.
+ *
+ * @see hook_taxonomy_vocabulary_predelete()
+ * @see hook_taxonomy_vocabulary_delete()
  */
 function taxonomy_vocabulary_delete($vid) {
   $vocabulary = taxonomy_vocabulary_load($vid);
 
   $transaction = db_transaction();
   try {
+    // Allow modules to act before vocabulary deletion.
+    module_invoke_all('taxonomy_vocabulary_predelete', $vocabulary);
+    module_invoke_all('entity_predelete', $vocabulary, 'taxonomy_vocabulary');
+
     // Only load terms without a parent, child terms will get deleted too.
     $result = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid = :vid AND th.parent = 0', array(':vid' => $vid))->fetchCol();
     foreach ($result as $tid) {
@@ -465,6 +472,8 @@ function taxonomy_vocabulary_delete($vid) {
       ->execute();
 
     field_attach_delete_bundle('taxonomy_term', $vocabulary->machine_name);
+
+    // Allow modules to respond to vocabulary deletion.
     module_invoke_all('taxonomy_vocabulary_delete', $vocabulary);
     module_invoke_all('entity_delete', $vocabulary, 'taxonomy_vocabulary');
 
@@ -659,6 +668,9 @@ function taxonomy_term_save($term) {
  *   The term ID.
  * @return
  *   Status constant indicating deletion.
+ *
+ * @see hook_taxonomy_term_predelete()
+ * @see hook_taxonomy_term_delete()
  */
 function taxonomy_term_delete($tid) {
   $transaction = db_transaction();
@@ -667,6 +679,12 @@ function taxonomy_term_delete($tid) {
     while ($tids) {
       $children_tids = $orphans = array();
       foreach ($tids as $tid) {
+        // Allow modules to act before term deletion.
+        if ($term = taxonomy_term_load($tid)) {
+          module_invoke_all('taxonomy_term_predelete', $term);
+          module_invoke_all('entity_predelete', $term, 'taxonomy_term');
+        }
+
         // See if any of the term's children are about to be become orphans:
         if ($children = taxonomy_get_children($tid)) {
           foreach ($children as $child) {
@@ -678,7 +696,7 @@ function taxonomy_term_delete($tid) {
           }
         }
 
-        if ($term = taxonomy_term_load($tid)) {
+        if ($term) {
           db_delete('taxonomy_term_data')
             ->condition('tid', $tid)
             ->execute();
@@ -687,6 +705,8 @@ function taxonomy_term_delete($tid) {
             ->execute();
 
           field_attach_delete('taxonomy_term', $term);
+
+          // Allow modules to respond to term deletion.
           module_invoke_all('taxonomy_term_delete', $term);
           module_invoke_all('entity_delete', $term, 'taxonomy_term');
           taxonomy_terms_static_reset();
@@ -1806,9 +1826,9 @@ function taxonomy_node_update($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function taxonomy_node_delete($node) {
+function taxonomy_node_predelete($node) {
   // Clean up the {taxonomy_index} table when nodes are deleted.
   taxonomy_delete_node_index($node);
 }
diff --git a/core/modules/tracker/tracker.module b/core/modules/tracker/tracker.module
index 227cf7209a36..89907b5833ae 100644
--- a/core/modules/tracker/tracker.module
+++ b/core/modules/tracker/tracker.module
@@ -177,9 +177,9 @@ function tracker_node_update($node, $arg = 0) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function tracker_node_delete($node, $arg = 0) {
+function tracker_node_predelete($node, $arg = 0) {
   db_delete('tracker_node')
     ->condition('nid', $node->nid)
     ->execute();
diff --git a/core/modules/translation/translation.module b/core/modules/translation/translation.module
index 9f6a6d5be091..db60a085bc12 100644
--- a/core/modules/translation/translation.module
+++ b/core/modules/translation/translation.module
@@ -387,9 +387,9 @@ function translation_node_validate($node, $form) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function translation_node_delete($node) {
+function translation_node_predelete($node) {
   // Only act if we are dealing with a content type supporting translations.
   if (translation_supported_type($node->type)) {
     translation_remove_from_set($node);
diff --git a/core/modules/trigger/trigger.module b/core/modules/trigger/trigger.module
index c0e516fcc07f..75f013319135 100644
--- a/core/modules/trigger/trigger.module
+++ b/core/modules/trigger/trigger.module
@@ -309,9 +309,9 @@ function trigger_node_insert($node) {
 }
 
 /**
- * Implements hook_node_delete().
+ * Implements hook_node_predelete().
  */
-function trigger_node_delete($node) {
+function trigger_node_predelete($node) {
   _trigger_node($node, 'node_delete');
 }
 
@@ -501,9 +501,9 @@ function trigger_user_cancel($edit, $account, $method) {
 }
 
 /**
- * Implements hook_user_delete().
+ * Implements hook_user_predelete().
  */
-function trigger_user_delete($account) {
+function trigger_user_predelete($account) {
   $edit = array();
   _trigger_user('user_delete', $edit, $account);
 }
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index 8cf788ce6de1..51237d73ac5e 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -31,25 +31,46 @@ function hook_user_load($users) {
 }
 
 /**
- * Respond to user deletion.
+ * Act before user deletion.
  *
- * This hook is invoked from user_delete_multiple() before field_attach_delete()
- * is called and before users are actually removed from the database.
+ * This hook is invoked from user_delete_multiple() before
+ * field_attach_delete() is called and before the user is actually removed from
+ * the database.
  *
  * Modules should additionally implement hook_user_cancel() to process stored
  * user data for other account cancellation methods.
  *
  * @param $account
- *   The account that is being deleted.
+ *   The account that is about to be deleted.
  *
+ * @see hook_user_delete()
  * @see user_delete_multiple()
  */
-function hook_user_delete($account) {
+function hook_user_predelete($account) {
   db_delete('mytable')
     ->condition('uid', $account->uid)
     ->execute();
 }
 
+/**
+ * Respond to user deletion.
+ *
+ * This hook is invoked from user_delete_multiple() after field_attach_delete()
+ * has been called and after the user has been removed from the database.
+ *
+ * Modules should additionally implement hook_user_cancel() to process stored
+ * user data for other account cancellation methods.
+ *
+ * @param $account
+ *   The account that has been deleted.
+ *
+ * @see hook_user_predelete()
+ * @see user_delete_multiple()
+ */
+function hook_user_delete($account) {
+  drupal_set_message(t('User: @name has been deleted.', array('@name' => $account->name)));
+}
+
 /**
  * Act on user account cancellations.
  *
@@ -60,7 +81,8 @@ function hook_user_delete($account) {
  * Modules may add further methods via hook_user_cancel_methods_alter().
  *
  * This hook is NOT invoked for the 'user_cancel_delete' account cancellation
- * method. To react on this method, implement hook_user_delete() instead.
+ * method. To react to that method, implement hook_user_predelete() or
+ * hook_user_delete() instead.
  *
  * Expensive operations should be added to the global account cancellation batch
  * by using batch_set().
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 2dded35cfb54..009a716e7bec 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -844,9 +844,9 @@ function user_file_move($file, $source) {
 }
 
 /**
- * Implements hook_file_delete().
+ * Implements hook_file_predelete().
  */
-function user_file_delete($file) {
+function user_file_predelete($file) {
   // Remove any references to the file.
   db_update('users')
     ->fields(array('picture' => 0))
@@ -2356,7 +2356,9 @@ function user_cancel($edit, $uid, $method) {
   );
   batch_set($batch);
 
-  // Modules use hook_user_delete() to respond to deletion.
+  // When the 'user_cancel_delete' method is used, user_delete() is called,
+  // which invokes hook_user_predelete() and hook_user_delete(). 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.
     module_invoke_all('user_cancel', $edit, $account, $method);
@@ -2436,6 +2438,9 @@ function user_delete($uid) {
  *
  * @param $uids
  *   An array of user IDs.
+ *
+ * @see hook_user_predelete()
+ * @see hook_user_delete()
  */
 function user_delete_multiple(array $uids) {
   if (!empty($uids)) {
@@ -2444,8 +2449,10 @@ function user_delete_multiple(array $uids) {
     $transaction = db_transaction();
     try {
       foreach ($accounts as $uid => $account) {
-        module_invoke_all('user_delete', $account);
-        module_invoke_all('entity_delete', $account, 'user');
+        // Allow modules to act prior to user deletion.
+        module_invoke_all('user_predelete', $account);
+        module_invoke_all('entity_predelete', $account, 'user');
+
         field_attach_delete('user', $account);
         drupal_session_destroy_uid($account->uid);
       }
@@ -2459,6 +2466,10 @@ function user_delete_multiple(array $uids) {
       db_delete('authmap')
         ->condition('uid', $uids, 'IN')
         ->execute();
+
+        // Allow modules to respond to user deletion.
+        module_invoke_all('user_delete', $account);
+        module_invoke_all('entity_delete', $account, 'user');
     }
     catch (Exception $e) {
       $transaction->rollback();
-- 
GitLab