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)) {
