Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.242 diff -u -p -r1.242 file.inc --- includes/file.inc 30 Nov 2010 19:31:46 -0000 1.242 +++ includes/file.inc 2 Dec 2010 07:04:21 -0000 @@ -579,6 +579,9 @@ function file_save(stdClass $file) { $file->original = entity_load_unchanged('file', $file->fid); } + module_invoke_all('file_presave', $file); + module_invoke_all('entity_presave', $file, 'file'); + if (empty($file->fid)) { drupal_write_record('file_managed', $file); // Inform modules about the newly added file. Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.924 diff -u -p -r1.924 comment.module --- modules/comment/comment.module 1 Dec 2010 00:19:18 -0000 1.924 +++ modules/comment/comment.module 2 Dec 2010 07:04:21 -0000 @@ -1452,6 +1452,7 @@ function comment_save($comment) { // Allow modules to alter the comment before saving. module_invoke_all('comment_presave', $comment); + module_invoke_all('entity_presave', $comment, 'comment'); if ($comment->cid) { // Update the comment in the database. Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1329 diff -u -p -r1.1329 node.module --- modules/node/node.module 1 Dec 2010 00:18:15 -0000 1.1329 +++ modules/node/node.module 2 Dec 2010 07:04:21 -0000 @@ -1058,6 +1058,7 @@ function node_save($node) { // Let modules modify the node before it is saved to the database. module_invoke_all('node_presave', $node); + module_invoke_all('entity_presave', $node, 'node'); if ($node->is_new || !empty($node->revision)) { // When inserting either a new node or a new node revision, $node->log Index: modules/simpletest/tests/entity_crud_hook_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/entity_crud_hook_test.module,v retrieving revision 1.1 diff -u -p -r1.1 entity_crud_hook_test.module --- modules/simpletest/tests/entity_crud_hook_test.module 15 Oct 2010 03:36:21 -0000 1.1 +++ modules/simpletest/tests/entity_crud_hook_test.module 2 Dec 2010 07:04:21 -0000 @@ -2,6 +2,59 @@ // $Id: entity_crud_hook_test.module,v 1.1 2010/10/15 03:36:21 webchick Exp $ // +// Presave hooks +// + +/** + * Implements hook_entity_presave(). + */ +function entity_crud_hook_test_entity_presave($entity, $type) { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $type); +} + +/** + * Implements hook_comment_presave(). + */ +function entity_crud_hook_test_comment_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +/** + * Implements hook_file_presave(). + */ +function entity_crud_hook_test_file_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +/** + * Implements hook_node_presave(). + */ +function entity_crud_hook_test_node_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +/** + * Implements hook_taxonomy_term_presave(). + */ +function entity_crud_hook_test_taxonomy_term_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +/** + * Implements hook_taxonomy_vocabulary_presave(). + */ +function entity_crud_hook_test_taxonomy_vocabulary_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +/** + * Implements hook_user_presave(). + */ +function entity_crud_hook_test_user_presave() { + $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); +} + +// // Insert hooks // Index: modules/simpletest/tests/entity_crud_hook_test.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/entity_crud_hook_test.test,v retrieving revision 1.1 diff -u -p -r1.1 entity_crud_hook_test.test --- modules/simpletest/tests/entity_crud_hook_test.test 15 Oct 2010 03:36:21 -0000 1.1 +++ modules/simpletest/tests/entity_crud_hook_test.test 2 Dec 2010 07:04:21 -0000 @@ -81,6 +81,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -94,6 +96,8 @@ class EntityCrudHookTestCase extends Dru $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'); @@ -123,6 +127,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -136,6 +142,8 @@ class EntityCrudHookTestCase extends Dru $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'); @@ -165,6 +173,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -178,6 +188,8 @@ class EntityCrudHookTestCase extends Dru $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'); @@ -209,6 +221,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -222,6 +236,8 @@ class EntityCrudHookTestCase extends Dru $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'); @@ -245,6 +261,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -258,6 +276,8 @@ class EntityCrudHookTestCase extends Dru $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'); @@ -283,6 +303,8 @@ class EntityCrudHookTestCase extends Dru $_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'); @@ -296,6 +318,8 @@ class EntityCrudHookTestCase extends Dru $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'); Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.218 diff -u -p -r1.218 system.api.php --- modules/system/system.api.php 1 Dec 2010 00:23:36 -0000 1.218 +++ modules/system/system.api.php 2 Dec 2010 07:04:21 -0000 @@ -276,6 +276,18 @@ function hook_entity_load($entities, $ty } /** + * Act on an entity before it is about to be created or updated. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being saved (i.e. node, user, comment). + */ +function hook_entity_presave($entity, $type) { + $entity->changed = REQUEST_TIME; +} + +/** * Act on entities when inserted. * * @param $entity @@ -2534,7 +2546,7 @@ function hook_file_validate(&$file) { } /** - * Respond to a file being added. + * Act on a file being inserted or updated. * * This hook is called when a file has been added to the database. The hook * doesn't distinguish between files created as a result of a copy or those @@ -2545,6 +2557,23 @@ function hook_file_validate(&$file) { * * @see file_save() */ +function hook_file_presave($file) { + // Change the file timestamp to an hour prior. + $file->timestamp -= 3600; +} + +/** + * Respond to a file being added. + * + * This hook is called before a file has been added to the database. The hook + * doesn't distinguish between files created as a result of a copy or those + * created by an upload. + * + * @param $file + * The file that is about to be saved. + * + * @see file_save() + */ function hook_file_insert($file) { } Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.622 diff -u -p -r1.622 taxonomy.module --- modules/taxonomy/taxonomy.module 30 Nov 2010 19:31:46 -0000 1.622 +++ modules/taxonomy/taxonomy.module 2 Dec 2010 07:04:21 -0000 @@ -404,6 +404,7 @@ function taxonomy_vocabulary_save($vocab } module_invoke_all('taxonomy_vocabulary_presave', $vocabulary); + module_invoke_all('entity_presave', $vocabulary, 'taxonomy_vocabulary'); if (!empty($vocabulary->vid) && !empty($vocabulary->name)) { $status = drupal_write_record('taxonomy_vocabulary', $vocabulary, 'vid'); @@ -554,6 +555,7 @@ function taxonomy_term_save($term) { field_attach_presave('taxonomy_term', $term); module_invoke_all('taxonomy_term_presave', $term); + module_invoke_all('entity_presave', $term, 'taxonomy_term'); if (empty($term->tid)) { $op = 'insert'; Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.1224 diff -u -p -r1.1224 user.module --- modules/user/user.module 30 Nov 2010 23:55:11 -0000 1.1224 +++ modules/user/user.module 2 Dec 2010 07:05:40 -0000 @@ -397,6 +397,8 @@ function user_load_by_name($name) { * * @return * A fully-loaded $user object upon successful save or FALSE if the save failed. + * + * @todo D8: Drop $edit and fix user_save() to be consistent with others. */ function user_save($account, $edit = array(), $category = 'account') { $transaction = db_transaction(); @@ -422,11 +424,6 @@ function user_save($account, $edit = arr $account->original = entity_load_unchanged('user', $account->uid); } - // Presave field allowing changing of $edit. - $edit = (object) $edit; - field_attach_presave('user', $edit); - $edit = (array) $edit; - if (empty($account)) { $account = new stdClass(); } @@ -438,8 +435,31 @@ function user_save($account, $edit = arr if (!empty($account->data)) { $edit['data'] = !empty($edit['data']) ? array_merge($account->data, $edit['data']) : $account->data; } + + // Invoke hook_user_presave() for all modules. user_module_invoke('presave', $edit, $account, $category); + // Invoke presave operations of Field Attach API and Entity API. As those + // APIs require an updated entity object, create that. + $account_unchanged = (array) $account; + foreach ($edit as $key => $value) { + $account->$key = $value; + } + field_attach_presave('user', $account); + module_invoke_all('entity_presave', $account, 'user'); + // Update $edit with any changes modules might have applied to $account, but + // ensure to keep everything in $edit that was originally there. + foreach ($account as $key => $value) { + if (!array_key_exists($key, $account_unchanged)) { + unset($account->$key); + $edit[$key] = $value; + } + elseif ($value !== $account_unchanged[$key]) { + $account->$key = $account_unchanged[$key]; + $edit[$key] = $value; + } + } + if (is_object($account) && !$account->is_new) { // Process picture uploads. if (!$delete_previous_picture = empty($edit['picture']->fid)) {