diff --git a/core/includes/file.inc b/core/includes/file.inc index c4fca6d..645c64b 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 0a6637c..3ff2eda 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 0591265..b381b6c 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 7793499..e5e636b 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 9b19477..e983778 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 cca1845..04c1f31 100644 --- a/core/modules/entity/entity.controller.inc +++ b/core/modules/entity/entity.controller.inc @@ -454,6 +454,9 @@ class EntityDatabaseStorageController extends DrupalDefaultEntityController impl 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 @@ class EntityDatabaseStorageController extends DrupalDefaultEntityController impl /** * 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 873a162..435f7bf 100644 --- a/core/modules/entity/tests/entity_crud_hook_test.module +++ b/core/modules/entity/tests/entity_crud_hook_test.module @@ -213,6 +213,59 @@ function entity_crud_hook_test_user_update() { } // +// 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 782201e..2bb459f 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 f5938a8..68204aa 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 3575bfd..26ab78f 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 ccc8cd1..a833027 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 b10a17a..1840cec 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 d526c48..60d1789 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,25 +449,44 @@ 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. * * This hook is invoked from node_revision_delete() after the revision has been diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 7c25666..2a6aaa3 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 34d2237..f946573 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 df11936..563bfc7 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 ec5452e..7fe34e4 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 bfeee56..21c7419 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 4f72553..191d82b 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 548622c..872b002 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 cb778c9..426306d 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 4eadf5a..191b519 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 227cf72..89907b5 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 9f6a6d5..db60a08 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 c0e516f..75f0133 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 8cf788c..51237d7 100644 --- a/core/modules/user/user.api.php +++ b/core/modules/user/user.api.php @@ -31,26 +31,47 @@ 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. * * This hook is invoked from user_cancel() before a user account is canceled. @@ -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 2dded35..009a716 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();