Index: CHANGELOG.txt
===================================================================
RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v
retrieving revision 1.292
diff -u -p -r1.292 CHANGELOG.txt
--- CHANGELOG.txt	22 Dec 2008 19:38:30 -0000	1.292
+++ CHANGELOG.txt	5 Jan 2009 04:59:36 -0000
@@ -32,6 +32,8 @@ Drupal 7.0, xxxx-xx-xx (development vers
     * Redesigned password strength validator.
     * Redesigned the add content type screen.
     * Highlight duplicate URL aliases.
+    * Added permission to allow users to cancel their own accounts, and
+      optionally also select the method for cancelling their accounts.
 - Performance:
     * Improved performance on uncached page views by loading multiple core
       objects in a single database query.
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.680
diff -u -p -r1.680 comment.module
--- modules/comment/comment.module	4 Jan 2009 16:19:39 -0000	1.680
+++ modules/comment/comment.module	5 Jan 2009 04:51:34 -0000
@@ -697,17 +697,31 @@ function comment_nodeapi_rss_item($node)
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function comment_user_delete(&$edit, &$user, $category = NULL) {
-  db_update('comment')
-    ->fields(array('uid' => 0))
-    ->condition('uid', $user->uid)
-    ->execute();
-  db_update('node_comment_statistics')
-    ->fields(array('last_comment_uid' => 0))
-    ->condition('last_comment_uid', $user->uid)
-    ->execute();
+function comment_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_BLOCK_UNPUBLISH:
+      db_update('comment')->fields(array('status' => 0))->condition('uid', $account->uid)->execute();
+      db_update('node_comment_statistics')->fields(array('last_comment_uid' => 0))->condition('last_comment_uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_ANONYMIZE:
+      db_update('comment')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      db_update('node_comment_statistics')->fields(array('last_comment_uid' => 0))->condition('last_comment_uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      module_load_include('inc', 'comment', 'comment.admin');
+      $comments = db_select('comment', 'c')->fields('c', array('cid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      foreach ($comments as $cid) {
+        $comment = comment_load($cid);
+        // Delete the comment and its replies.
+        _comment_delete_thread($comment);
+        _comment_update_node_statistics($comment->nid);
+      }
+      break;
+  }
 }
 
 /**
Index: modules/dblog/dblog.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/dblog/dblog.module,v
retrieving revision 1.31
diff -u -p -r1.31 dblog.module
--- modules/dblog/dblog.module	28 Dec 2008 20:41:19 -0000	1.31
+++ modules/dblog/dblog.module	3 Jan 2009 23:38:35 -0000
@@ -101,10 +101,18 @@ function dblog_cron() {
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function dblog_user_delete(&$edit, &$user) {
-  db_query('UPDATE {watchdog} SET uid = 0 WHERE uid = %d', $user->uid);
+function dblog_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_ANONYMIZE:
+      db_update('watchdog')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      db_delete('watchdog')->condition('uid', $account->uid)->execute();
+      break;
+  }
 }
 
 function _dblog_get_message_types() {
Index: modules/dblog/dblog.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v
retrieving revision 1.13
diff -u -p -r1.13 dblog.test
--- modules/dblog/dblog.test	24 Dec 2008 10:38:41 -0000	1.13
+++ modules/dblog/dblog.test	3 Jan 2009 00:26:19 -0000
@@ -165,7 +165,7 @@ class DBLogTestCase extends DrupalWebTes
     $this->doNode('page');
     $this->doNode('poll');
 
-    // When a user is deleted, any content they created remains but the
+    // When a user account is canceled, any content they created remains but the
     // uid = 0. Their blog entry shows as "'s blog" on the home page. Records
     // in the watchdog table related to that user have the uid set to zero.
   }
@@ -203,7 +203,7 @@ class DBLogTestCase extends DrupalWebTes
     $count_before = (isset($ids)) ? count($ids) : 0;
     $this->assertTrue($count_before > 0, t('DBLog contains @count records for @name', array('@count' => $count_before, '@name' => $user->name)));
     // Delete user.
-    user_delete(array(), $user->uid);
+    user_cancel(array(), $user->uid, USER_CANCEL_ANONYMIZE);
     // Count rows that have uids for the user.
     $count = db_result(db_query('SELECT COUNT(wid) FROM {watchdog} WHERE uid = %d', $user->uid));
     $this->assertTrue($count == 0, t('DBLog contains @count records for @name', array('@count' => $count, '@name' => $user->name)));
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1009
diff -u -p -r1.1009 node.module
--- modules/node/node.module	4 Jan 2009 19:56:51 -0000	1.1009
+++ modules/node/node.module	5 Jan 2009 04:51:35 -0000
@@ -1476,11 +1476,41 @@ function node_ranking() {
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function node_user_delete(&$edit, &$user) {
-  db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
-  db_query('UPDATE {node_revision} SET uid = 0 WHERE uid = %d', $user->uid);
+function node_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_BLOCK_UNPUBLISH:
+      // Unpublish nodes (current revisions).
+      module_load_include('inc', 'node', 'node.admin');
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      node_mass_update($nodes, array('status' => 0));
+      break;
+
+    case USER_CANCEL_ANONYMIZE:
+      // Anonymize nodes (current revisions).
+      module_load_include('inc', 'node', 'node.admin');
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      node_mass_update($nodes, array('uid' => 0));
+      // Anonymize old revisions.
+      db_update('node_revision')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      // Clean history.
+      db_delete('history')->condition('uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      // Delete nodes (current revisions).
+      // @todo Introduce node_mass_delete() or make node_mass_update() more flexible.
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      foreach ($nodes as $nid) {
+        node_delete($nid);
+      }
+      // Delete old revisions.
+      db_delete('node_revision')->condition('uid', $account->uid)->execute();
+      // Clean history.
+      db_delete('history')->condition('uid', $account->uid)->execute();
+      break;
+  }
 }
 
 /**
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.283
diff -u -p -r1.283 poll.module
--- modules/poll/poll.module	31 Dec 2008 12:02:23 -0000	1.283
+++ modules/poll/poll.module	3 Jan 2009 23:39:54 -0000
@@ -815,9 +815,17 @@ function poll_cancel($form, &$form_state
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function poll_user_delete(&$edit, &$user) {
-  db_query('UPDATE {poll_vote} SET uid = 0 WHERE uid = %d', $user->uid);
+function poll_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_ANONYMIZE:
+      db_update('poll_vote')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      db_delete('poll_vote')->condition('uid', $account->uid)->execute();
+      break;
+  }
 }
 
Index: modules/profile/profile.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v
retrieving revision 1.248
diff -u -p -r1.248 profile.module
--- modules/profile/profile.module	16 Dec 2008 23:57:33 -0000	1.248
+++ modules/profile/profile.module	3 Jan 2009 00:28:55 -0000
@@ -259,9 +259,9 @@ function profile_user_categories(&$edit,
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function profile_user_delete(&$edit, &$user, $category = NULL) {
+function profile_user_cancel(&$edit, &$user, $method) {
   db_query('DELETE FROM {profile_value} WHERE uid = %d', $user->uid);
 }
 
Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.292
diff -u -p -r1.292 statistics.module
--- modules/statistics/statistics.module	26 Dec 2008 14:23:38 -0000	1.292
+++ modules/statistics/statistics.module	3 Jan 2009 23:41:14 -0000
@@ -182,10 +182,18 @@ function statistics_menu() {
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function statistics_user_delete(&$edit, &$user, $category) {
-  db_query('UPDATE {accesslog} SET uid = 0 WHERE uid = %d', $user->uid);
+function statistics_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_ANONYMIZE:
+      db_update('accesslog')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      db_delete('accesslog')->condition('uid', $account->uid)->execute();
+      break;
+  }
 }
 
 /**
Index: modules/trigger/trigger.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/trigger/trigger.module,v
retrieving revision 1.25
diff -u -p -r1.25 trigger.module
--- modules/trigger/trigger.module	4 Jan 2009 16:10:48 -0000	1.25
+++ modules/trigger/trigger.module	5 Jan 2009 04:51:35 -0000
@@ -441,10 +441,15 @@ function trigger_user_update(&$edit, &$a
 }
 
 /**
- * Implementation of hook_user_delete().
+ * Implementation of hook_user_cancel().
  */
-function trigger_user_delete(&$edit, &$account, $category) {
-  _trigger_user('delete', $edit, $account, $category);
+function trigger_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_ANONYMIZE:
+    case USER_CANCEL_DELETE:
+      _trigger_user('delete', $edit, $account, $method);
+      break;
+  }
 }
 
 /**
Index: modules/user/user.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v
retrieving revision 1.33
diff -u -p -r1.33 user.admin.inc
--- modules/user/user.admin.inc	16 Nov 2008 15:10:49 -0000	1.33
+++ modules/user/user.admin.inc	5 Jan 2009 01:37:09 -0000
@@ -15,8 +15,8 @@ function user_admin($callback_arg = '') 
       $output = drupal_get_form('user_register');
       break;
     default:
-      if (!empty($_POST['accounts']) && isset($_POST['operation']) && ($_POST['operation'] == 'delete')) {
-        $output = drupal_get_form('user_multiple_delete_confirm');
+      if (!empty($_POST['accounts']) && isset($_POST['operation']) && ($_POST['operation'] == 'cancel')) {
+        $output = drupal_get_form('user_multiple_cancel_confirm');
       }
       else {
         $output = drupal_get_form('user_filter_form');
@@ -235,6 +235,20 @@ function user_admin_settings() {
   $form['registration']['user_email_verification'] = array('#type' => 'checkbox', '#title' => t('Require e-mail verification when a visitor creates an account'), '#default_value' => variable_get('user_email_verification', TRUE), '#description' => t('If this box is checked, new users will be required to validate their e-mail address prior to logging into the site, and will be assigned a system-generated password. With it unchecked, users will be logged in immediately upon registering, and may select their own passwords during registration.'));
   $form['registration']['user_registration_help'] = array('#type' => 'textarea', '#title' => t('User registration guidelines'), '#default_value' => variable_get('user_registration_help', ''), '#description' => t('This text is displayed at the top of the user registration form and is useful for helping or instructing your users.'));
 
+  // Cancel account settings.
+  module_load_include('inc', 'user', 'user.pages');
+  $form['cancel'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Cancel account settings'),
+  );
+  $form['cancel']['user_cancel_method'] = array(
+    '#type' => 'radios',
+    '#title' => t('When cancelling a user account'),
+    '#options' => user_cancel_methods(),
+    '#default_value' => variable_get('user_cancel_method', USER_CANCEL_BLOCK),
+    '#description' => t('This default applies to all users who want to cancel their accounts. Users with the %select-cancel-method or %administer-users <a href="@permissions-url">permissions</a> can override this default method.', array('%select-cancel-method' => t('Select method for cancelling account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/user/permissions'))),
+  );
+
   // User e-mail settings.
   $form['email'] = array(
     '#type' => 'fieldset',
@@ -243,7 +257,7 @@ function user_admin_settings() {
   );
   // These email tokens are shared for all settings, so just define
   // the list once to help ensure they stay in sync.
-  $email_token_help = t('Available variables are:') . ' !username, !site, !password, !uri, !uri_brief, !mailto, !date, !login_uri, !edit_uri, !login_url.';
+  $email_token_help = t('Available variables are:') . ' !username, !site, !password, !uri, !uri_brief, !mailto, !date, !login_uri, !edit_uri, !login_url, !cancel_url.';
 
   $form['email']['admin_created'] = array(
     '#type' => 'fieldset',
@@ -375,28 +389,48 @@ function user_admin_settings() {
     '#rows' => 3,
   );
 
-  $form['email']['deleted'] = array(
+  $form['email']['cancel_confirm'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Cancel account confirmation email'),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#description' => t('Customize e-mail messages sent to users when they attempt to cancel their accounts.') . ' ' . $email_token_help,
+  );
+  $form['email']['cancel_confirm']['user_mail_cancel_confirm_subject'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Subject'),
+    '#default_value' => _user_mail_text('cancel_confirm_subject'),
+    '#maxlength' => 180,
+  );
+  $form['email']['cancel_confirm']['user_mail_cancel_confirm_body'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Body'),
+    '#default_value' => _user_mail_text('cancel_confirm_body'),
+    '#rows' => 3,
+  );
+
+  $form['email']['canceled'] = array(
     '#type' => 'fieldset',
-    '#title' => t('Account deleted email'),
+    '#title' => t('Account canceled email'),
     '#collapsible' => TRUE,
     '#collapsed' => TRUE,
-    '#description' => t('Enable and customize e-mail messages sent to users when their accounts are deleted.') . ' ' . $email_token_help,
+    '#description' => t('Enable and customize e-mail messages sent to users when their accounts are canceled.') . ' ' . $email_token_help,
   );
-  $form['email']['deleted']['user_mail_status_deleted_notify'] = array(
+  $form['email']['canceled']['user_mail_status_canceled_notify'] = array(
     '#type' => 'checkbox',
-    '#title' => t('Notify user when account is deleted.'),
-    '#default_value' => variable_get('user_mail_status_deleted_notify', FALSE),
+    '#title' => t('Notify user when account is canceled.'),
+    '#default_value' => variable_get('user_mail_status_canceled_notify', FALSE),
   );
-  $form['email']['deleted']['user_mail_status_deleted_subject'] = array(
+  $form['email']['canceled']['user_mail_status_canceled_subject'] = array(
     '#type' => 'textfield',
     '#title' => t('Subject'),
-    '#default_value' => _user_mail_text('status_deleted_subject'),
+    '#default_value' => _user_mail_text('status_canceled_subject'),
     '#maxlength' => 180,
   );
-  $form['email']['deleted']['user_mail_status_deleted_body'] = array(
+  $form['email']['canceled']['user_mail_status_canceled_body'] = array(
     '#type' => 'textarea',
     '#title' => t('Body'),
-    '#default_value' => _user_mail_text('status_deleted_body'),
+    '#default_value' => _user_mail_text('status_canceled_body'),
     '#rows' => 3,
   );
 
Index: modules/user/user.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.api.php,v
retrieving revision 1.1
diff -u -p -r1.1 user.api.php
--- modules/user/user.api.php	25 Nov 2008 02:37:33 -0000	1.1
+++ modules/user/user.api.php	5 Jan 2009 05:36:17 -0000
@@ -23,8 +23,6 @@
  *     (probably along with 'insert') if you want to reuse some information from
  *     the user object.
  *   - "categories": A set of user information categories is requested.
- *   - "delete": The user account is being deleted. The module should remove its
- *     custom additions to the user object from the database.
  *   - "form": The user account edit form is about to be displayed. The module
  *     should present the form elements it wishes to inject into the form.
  *   - "insert": The user account is being added. The module should save its
@@ -88,6 +86,59 @@ function hook_user($op, &$edit, &$accoun
 }
 
 /**
+ * Act on user account cancellations.
+ *
+ * The user account is being canceled. Depending on the cancel account method,
+ * the module should either do nothing, unpublish contents, anonymize contents,
+ * or delete contents and data belonging to the canceled user account.
+ *
+ * Expensive operations should be added to the global batch.
+ *
+ * @param &$edit
+ *   The array of form values submitted by the user.
+ * @param &$account
+ *   The user object on which the operation is being performed.
+ * @param $method
+ *   The account cancellation method.
+ *
+ * @see user_cancel()
+ */
+function hook_user_cancel(&$edit, &$account, $method) {
+  switch ($method) {
+    case USER_CANCEL_BLOCK_UNPUBLISH:
+      // Unpublish nodes (current revisions).
+      module_load_include('inc', 'node', 'node.admin');
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      node_mass_update($nodes, array('status' => 0));
+      break;
+
+    case USER_CANCEL_ANONYMIZE:
+      // Anonymize nodes (current revisions).
+      module_load_include('inc', 'node', 'node.admin');
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      node_mass_update($nodes, array('uid' => 0));
+      // Anonymize old revisions.
+      db_update('node_revision')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute();
+      // Clean history.
+      db_delete('history')->condition('uid', $account->uid)->execute();
+      break;
+
+    case USER_CANCEL_DELETE:
+      // Delete nodes (current revisions).
+      // @todo Introduce node_mass_delete() or make node_mass_update() more flexible.
+      $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol();
+      foreach ($nodes as $nid) {
+        node_delete($nid);
+      }
+      // Delete old revisions.
+      db_delete('node_revision')->condition('uid', $account->uid)->execute();
+      // Clean history.
+      db_delete('history')->condition('uid', $account->uid)->execute();
+      break;
+  }
+}
+
+/**
  * Add mass user operations.
  *
  * This hook enables modules to inject custom operations into the mass operations
@@ -114,8 +165,8 @@ function hook_user_operations() {
       'label' => t('Block the selected users'),
       'callback' => 'user_user_operations_block',
     ),
-    'delete' => array(
-      'label' => t('Delete the selected users'),
+    'cancel' => array(
+      'label' => t('Cancel the selected user accounts'),
     ),
   );
   return $operations;
Index: modules/user/user.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.install,v
retrieving revision 1.15
diff -u -p -r1.15 user.install
--- modules/user/user.install	20 Nov 2008 06:56:17 -0000	1.15
+++ modules/user/user.install	3 Jan 2009 23:26:52 -0000
@@ -360,6 +360,31 @@ function user_update_7002(&$sandbox) {
 }
 
 /**
+ * Update user settings for cancelling user accounts.
+ *
+ * Prior 7.x, users were not able to cancel their accounts. When administrators
+ * deleted an account, all contents were assigned to uid 0 (Anonymous).
+ */
+function user_update_7003() {
+  $ret = array();
+  require_once DRUPAL_ROOT . drupal_get_path('module', 'user') . '/user.module';
+  variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+  if ($setting = variable_get('user_mail_status_deleted_notify', FALSE)) {
+    variable_set('user_mail_status_canceled_notify', $setting);
+    variable_del('user_mail_status_deleted_notify');
+  }
+  if ($setting = variable_get('user_mail_status_deleted_subject', FALSE)) {
+    variable_set('user_mail_status_canceled_subject', $setting);
+    variable_del('user_mail_status_deleted_subject');
+  }
+  if ($setting = variable_get('user_mail_status_deleted_body', FALSE)) {
+    variable_set('user_mail_status_canceled_body', $setting);
+    variable_del('user_mail_status_deleted_body');
+  }
+  return $ret;
+}
+
+/**
  * @} End of "defgroup user-updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.952
diff -u -p -r1.952 user.module
--- modules/user/user.module	4 Jan 2009 16:10:48 -0000	1.952
+++ modules/user/user.module	5 Jan 2009 04:51:35 -0000
@@ -17,6 +17,27 @@ define('USERNAME_MAX_LENGTH', 60);
 define('EMAIL_MAX_LENGTH', 64);
 
 /**
+ * Cancel account method: Disable user account and keep all contents.
+ */
+define('USER_CANCEL_BLOCK', 0);
+
+/**
+ * Cancel account method: Disable user account and unpublish all contents.
+ */
+define('USER_CANCEL_BLOCK_UNPUBLISH', 1);
+
+/**
+ * Cancel account method: Remove user account and anonymize all contents.
+ */
+define('USER_CANCEL_ANONYMIZE', 2);
+
+/**
+ * Cancel account method: Remove user account and remove all contents.
+ */
+define('USER_CANCEL_DELETE', 3);
+
+
+/**
  * Invokes hook_user() in every module.
  *
  * We cannot use module_invoke() for this, because the arguments need to
@@ -587,6 +608,14 @@ function user_perm() {
        'title' => t('Change own username'),
        'description' => t('Select a different username.'),
      ),
+     'cancel account' => array(
+       'title' => t('Cancel account'),
+       'description' => t('Remove or disable own user account and unpublish, anonymize, or remove own submissions depending on the configured <a href="@user-settings-url">user settings</a>.', array('@user-settings-url' => url('admin/user/settings'))),
+     ),
+     'select account cancelling method' => array(
+       'title' => t('Select method for cancelling own account'),
+       'description' => t('Select the method for cancelling own user account.'),
+     ),
    );
 }
 
@@ -948,6 +977,13 @@ function user_edit_access($account) {
   return (($GLOBALS['user']->uid == $account->uid) || user_access('administer users')) && $account->uid > 0;
 }
 
+/**
+ * Menu access callback; limit access to cancel account pages.
+ */
+function user_cancel_access($account) {
+  return ((($GLOBALS['user']->uid == $account->uid) && user_access('cancel account')) || user_access('administer users')) && $account->uid > 0;
+}
+
 function user_load_self($arg) {
   $arg[1] = user_load($GLOBALS['user']->uid);
   return $arg;
@@ -1082,12 +1118,21 @@ function user_menu() {
     'weight' => -10,
   );
 
-  $items['user/%user/delete'] = array(
-    'title' => 'Delete',
+  $items['user/%user/cancel'] = array(
+    'title' => 'Cancel account',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('user_confirm_delete', 1),
-    'access callback' => 'user_access',
-    'access arguments' => array('administer users'),
+    'page arguments' => array('user_cancel_confirm_form', 1),
+    'access callback' => 'user_cancel_access',
+    'access arguments' => array(1),
+    'type' => MENU_CALLBACK,
+  );
+
+  $items['user/%user/cancel/confirm'] = array(
+    'title' => 'Confirm to cancel account',
+    'page callback' => 'user_cancel_confirm',
+    'page arguments' => array(1, 4, 5, 6, 7),
+    'access callback' => 'user_cancel_access',
+    'access arguments' => array(1),
     'type' => MENU_CALLBACK,
   );
 
@@ -1445,6 +1490,23 @@ function user_pass_reset_url($account) {
   return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE));
 }
 
+/**
+ * Generate a URL to confirm a cancel account request.
+ *
+ * @see user_mail_tokens()
+ * @see user_cancel_confirm_form_submit()
+ */
+function user_cancel_url($account) {
+  // If tokens for other mails than the cancel account confirmation mail are
+  // generated, we need to use default values for the generated link.
+  if (!isset($account->user_cancel_method, $account->user_cancel_notify)) {
+    $account->user_cancel_method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $account->user_cancel_notify = variable_get('user_mail_status_canceled_notify', FALSE);
+  }
+  $timestamp = REQUEST_TIME;
+  return url("user/$account->uid/cancel/confirm/$timestamp/{$account->user_cancel_method}/{$account->user_cancel_notify}/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE));
+}
+
 function user_pass_rehash($password, $timestamp, $login) {
   return md5($timestamp . $password . $login);
 }
@@ -1595,21 +1657,106 @@ function _user_edit_submit($account, &$e
 }
 
 /**
- * Delete a user.
+ * Cancel a user account.
+ *
+ * Since the user cancellation process needs to be run in a batch, either
+ * Form API will invoke it, or batch_process() needs to be invoked after calling
+ * this function.
  *
- * @param $edit An array of submitted form values.
- * @param $uid The user ID of the user to delete.
+ * @param $edit
+ *   An array of submitted form values.
+ * @param $uid
+ *   The user ID of the user account to cancel.
+ * @param $method
+ *   The cancel account method to use.
+ *
+ * @see _user_cancel()
  */
-function user_delete($edit, $uid) {
+function user_cancel($edit, $uid, $method) {
+  global $user;
+
   $account = user_load(array('uid' => $uid));
-  drupal_session_destroy_uid($uid);
-  _user_mail_notify('status_deleted', $account);
-  module_invoke_all('user_delete', $edit, $account);
-  db_query('DELETE FROM {users} WHERE uid = %d', $uid);
-  db_query('DELETE FROM {users_roles} WHERE uid = %d', $uid);
-  db_query('DELETE FROM {authmap} WHERE uid = %d', $uid);
-  $variables = array('%name' => $account->name, '%email' => '<' . $account->mail . '>');
-  watchdog('user', 'Deleted user: %name %email.', $variables, WATCHDOG_NOTICE);
+
+  if (!$account) {
+    drupal_set_message(t('The user account %id does not exist.', array('%id' => $uid)), 'error');
+    watchdog('user', 'Attempted to cancel non-existing user account: %id.', array('%id' => $uid), WATCHDOG_ERROR);
+    return;
+  }
+
+  // Initialize batch (to set title).
+  $batch = array(
+    'title' => t('Cancelling account'),
+    'redirect' => ($account->uid == $user->uid ? '' : 'admin/user/user'),
+    'operations' => array(),
+  );
+  batch_set($batch);
+
+  // Allow modules to add further sets to this batch.
+  module_invoke_all('user_cancel', $edit, $account, $method);
+
+  // Finish the batch and actually cancel the account.
+  $batch = array(
+    'title' => t('Cancelling user account'),
+    'operations' => array(
+      array('_user_cancel', array($edit, $account, $method)),
+    ),
+  );
+  batch_set($batch);
+
+  // Batch processing is either handled via Form API or has to be invoked
+  // manually.
+}
+
+/**
+ * Last batch processing step for cancelling a user account.
+ *
+ * Since batch and session API require a valid user account, the actual
+ * cancellation of a user account needs to happen last.
+ *
+ * @see user_cancel()
+ */
+function _user_cancel($edit, $account, $method) {
+  global $user;
+
+  switch ($method) {
+    case USER_CANCEL_BLOCK:
+    case USER_CANCEL_BLOCK_UNPUBLISH:
+    default:
+      // Send account blocked notification if option was checked.
+      if (!empty($edit['user_cancel_notify'])) {
+        _user_mail_notify('status_blocked', $account);
+      }
+      db_update('users')->fields(array('status' => 0))->condition('uid', $account->uid)->execute();
+      drupal_set_message(t('%name has been disabled.', array('%name' => $account->name)));
+      break;
+
+    case USER_CANCEL_ANONYMIZE:
+    case USER_CANCEL_DELETE:
+      // Send account canceled notification if option was checked.
+      if (!empty($edit['user_cancel_notify'])) {
+        _user_mail_notify('status_canceled', $account);
+      }
+      db_delete('users')->condition('uid', $account->uid)->execute();
+      db_delete('users_roles')->condition('uid', $account->uid)->execute();
+      db_delete('authmap')->condition('uid', $account->uid)->execute();
+      drupal_set_message(t('%name has been deleted.', array('%name' => $account->name)));
+      $variables = array('%name' => $account->name, '%email' => '<' . $account->mail . '>');
+      watchdog('user', 'Deleted user: %name %email.', $variables, WATCHDOG_NOTICE);
+      break;
+  }
+
+  // After cancelling account, ensure that user is logged out.
+  if ($account->uid == $user->uid) {
+    // Destroy the current session.
+    session_destroy();
+    // Load the anonymous user.
+    $user = drupal_anonymous_user();
+  }
+  else {
+    drupal_session_destroy_uid($account->uid);
+  }
+  // Clear the cache for anonymous users.
+  cache_clear_all();
 }
 
 /**
@@ -1682,10 +1829,26 @@ function _user_mail_text($key, $language
         return t('Account details for !username at !site (blocked)', $variables, $langcode);
       case 'status_blocked_body':
         return t("!username,\n\nYour account on !site has been blocked.", $variables, $langcode);
-      case 'status_deleted_subject':
-        return t('Account details for !username at !site (deleted)', $variables, $langcode);
-      case 'status_deleted_body':
-        return t("!username,\n\nYour account on !site has been deleted.", $variables, $langcode);
+
+      case 'cancel_confirm_subject':
+        return t('Cancel account request for !username at !site', $variables, $langcode);
+      case 'cancel_confirm_body':
+        return t("!username,
+
+A request to cancel your account has been made at !site.
+
+You may now cancel your account on !uri_brief by clicking this link or copying and pasting it into your browser:
+
+!cancel_url
+
+This link expires in one day and nothing will happen if it is not used.", $variables, $langcode);
+
+      case 'status_canceled_subject':
+        return t('Account details for !username at !site (canceled)', $variables, $langcode);
+      case 'status_canceled_body':
+        return t("!username,
+
+Your account on !site has been canceled.", $variables, $langcode);
     }
   }
 }
@@ -1752,8 +1915,8 @@ function user_user_operations($form_stat
       'label' => t('Block the selected users'),
       'callback' => 'user_user_operations_block',
     ),
-    'delete' => array(
-      'label' => t('Delete the selected users'),
+    'cancel' => array(
+      'label' => t('Cancel the selected user accounts'),
     ),
   );
 
@@ -1866,7 +2029,7 @@ function user_multiple_role_edit($accoun
   }
 }
 
-function user_multiple_delete_confirm(&$form_state) {
+function user_multiple_cancel_confirm(&$form_state) {
   $edit = $form_state['post'];
 
   $form['accounts'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
@@ -1875,20 +2038,62 @@ function user_multiple_delete_confirm(&$
     $user = db_result(db_query('SELECT name FROM {users} WHERE uid = %d', $uid));
     $form['accounts'][$uid] = array('#type' => 'hidden', '#value' => $uid, '#prefix' => '<li>', '#suffix' => check_plain($user) . "</li>\n");
   }
-  $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
+
+  $form['operation'] = array('#type' => 'hidden', '#value' => 'cancel');
+
+  module_load_include('inc', 'user', 'user.pages');
+  $form['user_cancel_method'] = array(
+    '#type' => 'radios',
+    '#title' => t('When cancelling these accounts'),
+    '#options' => user_cancel_methods(),
+    '#default_value' => variable_get('user_cancel_method', USER_CANCEL_BLOCK),
+    '#required' => TRUE,
+  );
+
+  // Allow to send the cancel account confirmation mail.
+  $form['user_cancel_confirm'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Send cancel account confirmation e-mail.'),
+    '#default_value' => FALSE,
+    '#description' => t('When enabled, the user needs to confirm the chosen account cancelling method via e-mail.'),
+  );
+  // Also allow to send account canceled notification mail, if enabled.
+  $form['user_cancel_notify'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Send account canceled notification e-mail.'),
+    '#default_value' => FALSE,
+    '#access' => variable_get('user_mail_status_canceled_notify', FALSE),
+    '#description' => t('When enabled, the user will receive an e-mail notification about the canceled account.'),
+  );
 
   return confirm_form($form,
-                      t('Are you sure you want to delete these users?'),
+                      t('Are you sure you want to cancel these user accounts?'),
                       'admin/user/user', t('This action cannot be undone.'),
-                      t('Delete all'), t('Cancel'));
+                      t('Cancel accounts'), t('Cancel'));
 }
 
-function user_multiple_delete_confirm_submit($form, &$form_state) {
+/**
+ * Submit handler for mass-account cancellation form.
+ *
+ * @see user_multiple_cancel_confirm()
+ * @see user_cancel_confirm_form_submit()
+ */
+function user_multiple_cancel_confirm_submit($form, &$form_state) {
+  global $user;
+
   if ($form_state['values']['confirm']) {
     foreach ($form_state['values']['accounts'] as $uid => $value) {
-      user_delete($form_state['values'], $uid);
+      // Prevent user administrators from deleting themselves without confirmation.
+      if ($uid == $user->uid) {
+        $admin_form_state = $form_state;
+        unset($admin_form_state['values']['user_cancel_confirm']);
+        $admin_form_state['values']['_account'] = $user;
+        user_cancel_confirm_form_submit(array(), $admin_form_state);
+      }
+      else {
+        user_cancel($form_state['values'], $uid, $form_state['values']['user_cancel_method']);
+      }
     }
-    drupal_set_message(t('The users have been deleted.'));
   }
   $form_state['redirect'] = 'admin/user/user';
   return;
@@ -2082,6 +2287,7 @@ function user_mail_tokens($account, $lan
     '!username' => $account->name,
     '!site' => variable_get('site_name', 'Drupal'),
     '!login_url' => user_pass_reset_url($account),
+    '!cancel_url' => user_cancel_url($account),
     '!uri' => $base_url,
     '!uri_brief' => preg_replace('!^https?://!', '', $base_url),
     '!mailto' => $account->mail,
@@ -2144,8 +2350,8 @@ function user_preferred_language($accoun
  *  The return value from drupal_mail_send(), if ends up being called.
  */
 function _user_mail_notify($op, $account, $language = NULL) {
-  // By default, we always notify except for deleted and blocked.
-  $default_notify = ($op != 'status_deleted' && $op != 'status_blocked');
+  // By default, we always notify except for canceled and blocked.
+  $default_notify = ($op != 'status_canceled' && $op != 'status_blocked');
   $notify = variable_get('user_mail_' . $op . '_notify', $default_notify);
   if ($notify) {
     $params['account'] = $account;
Index: modules/user/user.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v
retrieving revision 1.25
diff -u -p -r1.25 user.pages.inc
--- modules/user/user.pages.inc	30 Dec 2008 16:43:20 -0000	1.25
+++ modules/user/user.pages.inc	5 Jan 2009 01:48:03 -0000
@@ -228,9 +228,10 @@ function user_edit($account, $category =
  * @ingroup forms
  * @see user_profile_form_validate()
  * @see user_profile_form_submit()
- * @see user_edit_delete_submit()
+ * @see user_cancel_confirm_form_submit()
  */
 function user_profile_form($form_state, $account, $category = 'account') {
+  global $user;
 
   $edit = (empty($form_state['values'])) ? (array)$account : $form_state['values'];
 
@@ -238,12 +239,12 @@ function user_profile_form($form_state, 
   $form['_category'] = array('#type' => 'value', '#value' => $category);
   $form['_account'] = array('#type' => 'value', '#value' => $account);
   $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 30);
-  if (user_access('administer users')) {
-    $form['delete'] = array(
+  if (($account->uid == $user->uid && user_access('cancel account')) || user_access('administer users')) {
+    $form['cancel'] = array(
       '#type' => 'submit',
-      '#value' => t('Delete'),
+      '#value' => t('Cancel account'),
       '#weight' => 31,
-      '#submit' => array('user_edit_delete_submit'),
+      '#submit' => array('user_edit_cancel_submit'),
     );
   }
   $form['#attributes']['enctype'] = 'multipart/form-data';
@@ -282,45 +283,210 @@ function user_profile_form_submit($form,
 }
 
 /**
- * Submit function for the 'Delete' button on the user edit form.
+ * Submit function for the 'Cancel account' button on the user edit form.
  */
-function user_edit_delete_submit($form, &$form_state) {
+function user_edit_cancel_submit($form, &$form_state) {
   $destination = '';
   if (isset($_REQUEST['destination'])) {
     $destination = drupal_get_destination();
     unset($_REQUEST['destination']);
   }
-  // Note: We redirect from user/uid/edit to user/uid/delete to make the tabs disappear.
-  $form_state['redirect'] = array("user/" . $form_state['values']['_account']->uid . "/delete", $destination);
+  // Note: We redirect from user/uid/edit to user/uid/cancel to make the tabs disappear.
+  $form_state['redirect'] = array("user/" . $form_state['values']['_account']->uid . "/cancel", $destination);
 }
 
 /**
- * Form builder; confirm form for user deletion.
+ * Form builder; confirm form for cancelling user account.
  *
  * @ingroup forms
- * @see user_confirm_delete_submit()
+ * @see user_edit_cancel_submit()
  */
-function user_confirm_delete(&$form_state, $account) {
+function user_cancel_confirm_form(&$form_state, $account) {
+  global $user;
 
   $form['_account'] = array('#type' => 'value', '#value' => $account);
 
+  // Display cancel account method selection, if allowed.
+  $default_method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+  if (user_access('select account cancelling method') || user_access('administer users')) {
+    extract(user_cancel_methods($account));
+    $form['user_cancel_method'] = array(
+      '#type' => 'radios',
+      '#title' => ($account->uid == $user->uid ? t('When cancelling your account') : t('When cancelling the account')),
+      '#options' => user_cancel_methods(),
+      '#default_value' => $default_method,
+      '#required' => TRUE,
+    );
+  }
+  else {
+    extract(user_cancel_methods($account, $default_method));
+    $form['user_cancel_method'] = array('#type' => 'value', '#value' => $default_method);
+  }
+
+  // Allow user account administrators to skip the cancel account confirmation
+  // mail (by default), as long as they do not attempt to cancel their own
+  // accounts.
+  if (user_access('administer users') && $account->uid != $user->uid) {
+    $form['user_cancel_confirm'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Send cancel account confirmation e-mail.'),
+      '#default_value' => FALSE,
+      '#description' => t('When enabled, the user needs to confirm the chosen account cancelling method via e-mail.'),
+    );
+    // Also allow to send account canceled notification mail, if enabled.
+    if (variable_get('user_mail_status_canceled_notify', FALSE)) {
+      $form['user_cancel_notify'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Send account canceled notification e-mail.'),
+        '#default_value' => FALSE,
+        '#description' => t('When enabled, the user will receive an e-mail notification about the canceled account.'),
+      );
+    }
+  }
+  else {
+    $form['user_cancel_confirm'] = array('#type' => 'value', '#value' => TRUE);
+    $form['user_cancel_notify'] = array('#type' => 'value', '#value' => TRUE);
+  }
+
   return confirm_form($form,
-    t('Are you sure you want to delete the account %name?', array('%name' => $account->name)),
+    $question,
     'user/' . $account->uid,
-    t('All submissions made by this user will be attributed to the anonymous account. This action cannot be undone.'),
-    t('Delete'), t('Cancel'));
+    $description . ' ' . t('This action cannot be undone.'),
+    t('Cancel account'), t('Cancel'));
 }
 
 /**
  * Submit function for the confirm form for user deletion.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_multiple_cancel_confirm_submit()
+ */
+function user_cancel_confirm_form_submit($form, &$form_state) {
+  global $user;
+  $account = $form_state['values']['_account'];
+
+  // Cancel account immediately, if the current user has administrative
+  // privileges, no confirmation mail shall be sent, and the user does not
+  // attempt to cancel the own account.
+  if (user_access('administer users') && empty($form_state['values']['user_cancel_confirm']) && $account->uid != $user->uid) {
+    user_cancel($form_state['values'], $account->uid, $form_state['values']['user_cancel_method']);
+
+    if (!isset($_REQUEST['destination'])) {
+      $form_state['redirect'] = 'admin/user/user';
+    }
+  }
+  else {
+    // Store cancelling method and whether to notify the user in $account for
+    // user_cancel_url().
+    $account->user_cancel_method = $form_state['values']['user_cancel_method'];
+    $account->user_cancel_notify = $form_state['values']['user_cancel_notify'];
+    _user_mail_notify('cancel_confirm', $account);
+    drupal_set_message(t('A confirmation request to cancel your account has been sent to your e-mail address.'));
+
+    if (!isset($_REQUEST['destination'])) {
+      $form_state['redirect'] = "user/$account->uid";
+    }
+  }
+}
+
+/**
+ * Helper function to return cancel account methods.
+ *
+ * @param $account
+ *   (optional) The user account to cancel.
+ * @param $method
+ *   (optional) The cancel account method to use.
+ * @return
+ *   If no argument is passed in, all cancel account methods are returned. If
+ *   $account and optionally $method are passed, an array containing 'question'
+ *   and 'description' for a confirmation form is returned.
+ *
+ * @see user_admin_settings()
+ * @see user_cancel_confirm_form()
  */
-function user_confirm_delete_submit($form, &$form_state) {
-  user_delete($form_state['values'], $form_state['values']['_account']->uid);
-  drupal_set_message(t('%name has been deleted.', array('%name' => $form_state['values']['_account']->name)));
+function user_cancel_methods($account = NULL, $method = NULL) {
+  global $user;
+  
+  // If no argument was given, all methods are returned, suitable to be used as
+  // FAPI #options array.
+  if (!isset($account)) {
+    return array(
+      USER_CANCEL_BLOCK           => t('Disable the account and keep all content.'),
+      USER_CANCEL_BLOCK_UNPUBLISH => t('Disable the account and unpublish all content.'),
+      USER_CANCEL_ANONYMIZE       => t('Delete the account and make all content belong to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+      USER_CANCEL_DELETE          => t('Delete the account and all content.'),
+    );
+  }
+
+  // The confirmation form page title is the same for all methods.
+  $question = ($account->uid == $user->uid ? t('Are you sure you want to cancel your account?') : t('Are you sure you want to cancel the account %name?', array('%name' => $account->name)));
 
-  if (!isset($_REQUEST['destination'])) {
-    $form_state['redirect'] = 'admin/user/user';
+  // If no $method was passed, the user is allowed to choose from the options.
+  if (!isset($method)) {
+    return array(
+      'question' => $question,
+      'description' => t('Select the method to cancel the account above.'),
+    );
+  }
+
+  // If we end up here, then the current user tries to cancel the own account.
+  switch ($method) {
+    case USER_CANCEL_BLOCK:
+    default:
+      return array(
+        'question' => $question,
+        'description' => '',
+      );
+
+    case USER_CANCEL_BLOCK_UNPUBLISH:
+      return array(
+        'question' => $question,
+        'description' => t('All your content will be unpublished.'),
+      );
+
+    case USER_CANCEL_ANONYMIZE:
+      return array(
+        'question' => $question,
+        'description' => t('All your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+      );
+
+    case USER_CANCEL_DELETE:
+      return array(
+        'question' => $question,
+        'description' => t('All your content will be deleted.'),
+      );
+  }
+}
+
+/**
+ * Menu callback; Cancel a user account via email confirmation link.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_cancel_url()
+ */
+function user_cancel_confirm($account, $timestamp, $method, $notify, $hashed_pass) {
+  // Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
+  $timeout = 86400;
+  $current = REQUEST_TIME;
+
+  // Basic validation of arguments.
+  if (!empty($timestamp) && !empty($hashed_pass)) {
+    // Validate expiration and hashed password/login.
+    if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
+      $edit = array();
+      user_cancel($edit, $account->uid, $method);
+      // Since user_cancel() is not invoked via Form API, batch processing needs
+      // to be invoked manually.
+      batch_process();
+
+      drupal_goto('');
+    }
+    else {
+      drupal_set_message(t('You have tried to use a cancel account link that has expired. Please request a new one using the form below.'));
+      drupal_goto("user/$account->uid/cancel");
+    }
   }
+  drupal_access_denied();
 }
 
 function user_edit_validate($form, &$form_state) {
Index: modules/user/user.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.test,v
retrieving revision 1.24
diff -u -p -r1.24 user.test
--- modules/user/user.test	30 Dec 2008 16:43:20 -0000	1.24
+++ modules/user/user.test	5 Jan 2009 05:29:16 -0000
@@ -151,45 +151,325 @@ class UserValidationTestCase extends Dru
   }
 }
 
-
-class UserDeleteTestCase extends DrupalWebTestCase {
+class UserCancelTestCase extends DrupalWebTestCase {
   function getInfo() {
     return array(
-      'name' => t('User delete'),
-      'description' => t('Registers a user and deletes it.'),
-      'group' => t('User')
+      'name' => t('Cancel account'),
+      'description' => t('Ensure that cancel account methods work as expected.'),
+      'group' => t('User'),
     );
   }
 
   /**
-   * Registers a user and deletes it.
+   * Cancel account without permission.
    */
-  function testUserRegistration() {
-    // Set user registration to "Visitors can create accounts and no administrator approval is required."
-    variable_set('user_register', 1);
+  function testUserCancelWithoutPermission() {
+    variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
 
-    $edit = array();
-    $edit['name'] = $this->randomName();
-    $edit['mail'] = $edit['name'] . '@example.com';
-    $this->drupalPost('user/register', $edit, t('Create new account'));
-    $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'), t('User registered successfully.'));
+    // Create a user.
+    $user = $this->drupalCreateUser(array());
+    $this->drupalLogin($user);
+    $timestamp = time();
+    // Update user object.
+    $old_user = $user;
+    $user = user_load($user->uid);
+    $user->pass_raw = $old_user->pass_raw;
 
-    $user = user_load($edit);
+    // Create a node.
+    $node = $this->drupalCreateNode(array('uid' => $user->uid));
 
-    // Create admin user to delete registered user.
+    // Attempt to cancel account.
+    $this->drupalGet('user/' . $user->uid . '/edit');
+    $this->assertNoRaw(t('Cancel account'), t('No cancel account button displayed.'));
+
+    // Attempt bogus cancel account request confirmation.
+    $method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $notify = 1;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$timestamp/$method/$notify/" . user_pass_rehash($user->pass, $timestamp, $user->login));
+    $this->assertResponse(403, t('Bogus cancelling request rejected.'));
+    $this->assertTrue(user_load(array('uid' => $user->uid, 'status' => 1)), t('User account was not canceled.'));
+
+    // Confirm user's content has not been altered.
+    $test_node = node_load($node->nid, NULL, TRUE);
+    $this->assertTrue(($test_node->uid == $user->uid && $test_node->status == 1), t('Node of the user has not been altered.'));
+  }
+
+  /**
+   * Disable user account and keep all contents.
+   */
+  function testUserBlock() {
+    variable_set('user_cancel_method', USER_CANCEL_BLOCK);
+
+    // Create a user.
+    $user = $this->drupalCreateUser(array('cancel account'));
+    $this->drupalLogin($user);
+    $timestamp = time();
+    // Update user object.
+    $old_user = $user;
+    $user = user_load($user->uid);
+    $user->pass_raw = $old_user->pass_raw;
+
+    // Attempt to cancel account.
+    $this->drupalGet('user/' . $user->uid . '/edit');
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('Are you sure you want to cancel your account?'), t('[confirm deletion] Asks for confirmation.'));
+    $this->assertText(t('This action cannot be undone.'), t('[confirm deletion] Inform that action cannot be undone.'));
+
+    // Confirm to cancel account.
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Cancel account request mailed message displayed.'));
+
+    // Confirm cancel account request.
+    $method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $notify = 1;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$timestamp/$method/$notify/" . user_pass_rehash($user->pass, $timestamp, $user->login));
+    $this->assertTrue(user_load(array('uid' => $user->uid, 'status' => 0)), t('User has been blocked.'));
+
+    // Confirm user is logged out.
+    $this->assertNoText($user->name, t('Logged out.'));
+  }
+
+  /**
+   * Disable user account and unpublish all contents.
+   */
+  function testUserBlockUnpublish() {
+    variable_set('user_cancel_method', USER_CANCEL_BLOCK_UNPUBLISH);
+
+    // Create a user.
+    $user = $this->drupalCreateUser(array('cancel account'));
+    $this->drupalLogin($user);
+    $timestamp = time();
+    // Update user object.
+    $old_user = $user;
+    $user = user_load($user->uid);
+    $user->pass_raw = $old_user->pass_raw;
+
+    // Create a node with two revisions.
+    $node = $this->drupalCreateNode(array('uid' => $user->uid));
+    $settings = get_object_vars($node);
+    $settings['revision'] = 1;
+    $node = $this->drupalCreateNode($settings);
+
+    // Attempt to cancel account.
+    $this->drupalGet('user/' . $user->uid . '/edit');
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('Are you sure you want to cancel your account?'), t('[confirm deletion] Asks for confirmation.'));
+    $this->assertText(t('All your content will be unpublished. This action cannot be undone.'), t('[confirm deletion] Inform that all content will be unpublished.'));
+
+    // Confirm to cancel account.
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Cancel account request mailed message displayed.'));
+
+    // Confirm cancel account request.
+    $method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $notify = 1;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$timestamp/$method/$notify/" . user_pass_rehash($user->pass, $timestamp, $user->login));
+    $this->assertTrue(user_load(array('uid' => $user->uid, 'status' => 0)), t('User has been blocked.'));
+
+    // Confirm user's content has been unpublished.
+    $test_node = node_load($node->nid, NULL, TRUE);
+    $this->assertTrue($test_node->status == 0, t('Node of the user has been unpublished.'));
+    $test_node = node_load($node->nid, $node->vid, TRUE);
+    $this->assertTrue($test_node->status == 0, t('Node revision of the user has been unpublished.'));
+
+    // Confirm user is logged out.
+    $this->assertNoText($user->name, t('Logged out.'));
+  }
+
+  /**
+   * Remove user account and anonymize all contents.
+   */
+  function testUserAnonymize() {
+    variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+
+    // Create a user.
+    $user = $this->drupalCreateUser(array('cancel account'));
+    $this->drupalLogin($user);
+    $timestamp = time();
+    // Update user object.
+    $old_user = $user;
+    $user = user_load($user->uid);
+    $user->pass_raw = $old_user->pass_raw;
+
+    // Create a simple node.
+    $node = $this->drupalCreateNode(array('uid' => $user->uid));
+
+    // Create a node with two revisions, the initial one belonging to the
+    // cancelling user.
+    $revision_node = $this->drupalCreateNode(array('uid' => $user->uid));
+    $revision = $revision_node->vid;
+    $settings = get_object_vars($revision_node);
+    $settings['revision'] = 1;
+    $settings['uid'] = 1; // Set new/current revision to someone else.
+    $revision_node = $this->drupalCreateNode($settings);
+
+    // Attempt to cancel account.
+    $this->drupalGet('user/' . $user->uid . '/edit');
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('Are you sure you want to cancel your account?'), t('[confirm deletion] Asks for confirmation.'));
+    $this->assertRaw(t('All your content will be assigned to the %anonymous-name user. This action cannot be undone.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), t('[confirm deletion] Inform that all content will be attributed to anonymous account.'));
+
+    // Confirm to cancel account.
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Cancel account request mailed message displayed.'));
+
+    // Attempt bogus cancel account request confirmation.
+    $bogus_timestamp = $timestamp + 60;
+    $method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $notify = 1;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$bogus_timestamp/$method/$notify/" . user_pass_rehash($user->pass, $bogus_timestamp, $user->login));
+    $this->assertText(t('You have tried to use a cancel account link that has expired. Please request a new one using the form below.'), t('Bogus cancelling request rejected.'));
+    $this->assertTrue(user_load(array('uid' => $user->uid, 'status' => 1)), t('User account was not canceled.'));
+
+    // Attempt expired cancel account request confirmation.
+    $bogus_timestamp = $timestamp - 86400 - 60;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$bogus_timestamp/$method/$notify/" . user_pass_rehash($user->pass, $bogus_timestamp, $user->login));
+    $this->assertText(t('You have tried to use a cancel account link that has expired. Please request a new one using the form below.'), t('Expired cancel account request rejected.'));
+    $this->assertTrue(user_load(array('uid' => $user->uid, 'status' => 1)), t('User account was not canceled.'));
+
+    // Confirm cancel account request.
+    $this->drupalGet("user/$user->uid/cancel/confirm/$timestamp/$method/$notify/" . user_pass_rehash($user->pass, $timestamp, $user->login));
+    $this->assertFalse(user_load($user->uid), t('User is not found in the database.'));
+
+    // Confirm user's content has been attributed to anonymous user.
+    $test_node = node_load($node->nid, NULL, TRUE);
+    $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node of the user has been attributed to anonymous user.'));
+    $test_node = node_load($revision_node->nid, $revision, TRUE);
+    $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node revision of the user has been attributed to anonymous user.'));
+    $test_node = node_load($revision_node->nid, NULL, TRUE);
+    $this->assertTrue(($test_node->uid != 0 && $test_node->status == 1), t("Current revision of the user's node was not attributed to anonymous user."));
+
+    // Confirm user is logged out.
+    $this->assertNoText($user->name, t('Logged out.'));
+  }
+
+  /**
+   * Remove user account and remove all contents.
+   */
+  function testUserDelete() {
+    variable_set('user_cancel_method', USER_CANCEL_DELETE);
+
+    // Create a user.
+    $user = $this->drupalCreateUser(array('cancel account'));
+    $this->drupalLogin($user);
+    $timestamp = time();
+    // Update user object.
+    $old_user = $user;
+    $user = user_load($user->uid);
+    $user->pass_raw = $old_user->pass_raw;
+
+    // Create a simple node.
+    $node = $this->drupalCreateNode(array('uid' => $user->uid));
+
+    // Create comment.
+    module_load_include('test', 'comment');
+    $comment = CommentHelperCase::postComment($node, '', $this->randomName(32), TRUE, TRUE);
+    $this->assertTrue(comment_load($comment->id), t('Comment found.'));
+
+    // Create a node with two revisions, the initial one belonging to the
+    // cancelling user.
+    $revision_node = $this->drupalCreateNode(array('uid' => $user->uid));
+    $revision = $revision_node->vid;
+    $settings = get_object_vars($revision_node);
+    $settings['revision'] = 1;
+    $settings['uid'] = 1; // Set new/current revision to someone else.
+    $revision_node = $this->drupalCreateNode($settings);
+
+    // Attempt to cancel account.
+    $this->drupalGet('user/' . $user->uid . '/edit');
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('Are you sure you want to cancel your account?'), t('[confirm deletion] Asks for confirmation.'));
+    $this->assertText(t('All your content will be deleted. This action cannot be undone.'), t('[confirm deletion] Inform that all content will be deleted.'));
+
+    // Confirm to cancel account.
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Cancel account request mailed message displayed.'));
+
+    // Confirm cancel account request.
+    $method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+    $notify = 1;
+    $this->drupalGet("user/$user->uid/cancel/confirm/$timestamp/$method/$notify/" . user_pass_rehash($user->pass, $timestamp, $user->login));
+    $this->assertFalse(user_load($user->uid), t('User is not found in the database.'));
+
+    // Confirm user's content has been deleted.
+    $this->assertFalse(node_load($node->nid, NULL, TRUE), t('Node of the user has been deleted.'));
+    $this->assertFalse(node_load($node->nid, $revision, TRUE), t('Node revision of the user has been deleted.'));
+    $this->assertTrue(node_load($revision_node->nid, NULL, TRUE), t("Current revision of the user's node was not deleted."));
+    $this->assertFalse(comment_load($comment->id), t('Comment of the user has been deleted.'));
+
+    // Confirm user is logged out.
+    $this->assertNoText($user->name, t('Logged out.'));
+  }
+
+  /**
+   * Create an administrative user and delete another user.
+   */
+  function testUserCancelByAdmin() {
+    variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+
+    // Create a regular user.
+    $user = $this->drupalCreateUser(array());
+    
+    // Create administrative user.
     $admin_user = $this->drupalCreateUser(array('administer users'));
     $this->drupalLogin($admin_user);
 
-    // Delete user.
+    // Delete regular user.
     $this->drupalGet('user/' . $user->uid . '/edit');
-    $this->drupalPost(NULL, NULL, t('Delete'));
-    $this->assertRaw(t('Are you sure you want to delete the account %name?', array('%name' => $user->name)), t('[confirm deletion] Asks for confirmation.'));
-    $this->assertText(t('All submissions made by this user will be attributed to the anonymous account. This action cannot be undone.'), t('[confirm deletion] Inform that all submissions will be attributed to anonymous account.'));
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertRaw(t('Are you sure you want to cancel the account %name?', array('%name' => $user->name)), t('[confirm deletion] Asks for confirmation.'));
+    $this->assertText(t('Select the method to cancel the account above. This action cannot be undone.'), t('[confirm deletion] Allows to select cancel account method.'));
+
+    // Confirm deletion.
+    $this->drupalPost(NULL, NULL, t('Cancel account'));
+    $this->assertRaw(t('%name has been deleted.', array('%name' => $user->name)), t('User deleted.'));
+    $this->assertFalse(user_load($user->uid), t('User is not found in the database.'));
+  }
+
+  /**
+   * Create an administrative user and mass-delete other users.
+   */
+  function testMassUserCancelByAdmin() {
+    // Set cancel account method to "Remove user account and anonymize all contents."
+    variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+    // Enable Cancel account notification.
+    variable_set('user_mail_status_canceled_notify', TRUE);
+
+    // Create administrative user.
+    $admin_user = $this->drupalCreateUser(array('administer users'));
+    $this->drupalLogin($admin_user);
+
+    // Create some users.
+    $users = array();
+    for ($i = 0; $i < 3; $i++) {
+      $user = $this->drupalCreateUser(array());
+      $users[$user->uid] = $user;
+    }
+
+    // Cancel user accounts, including own one.
+    $edit = array();
+    $edit['operation'] = 'cancel';
+    foreach ($users as $uid => $user) {
+      $edit['accounts['. $uid .']'] = TRUE;
+    }
+    $edit['accounts['. $admin_user->uid .']'] = TRUE;
+    $this->drupalPost('admin/user/user', $edit, t('Update'));
+    $this->assertText(t('Are you sure you want to cancel these user accounts?'), t('[confirm cancellation] Asks for confirmation.'));
+    $this->assertText(t('When cancelling these accounts'), t('[confirm cancellation] Allows to select cancel account method.'));
+    $this->assertText(t('Send cancel account confirmation e-mail.'), t('[confirm cancellation] Allows to send confirmation mail.'));
+    $this->assertText(t('Send account canceled notification e-mail.'), t('[confirm cancellation] Allows to send notification mail.'));
 
     // Confirm deletion.
-    $this->drupalPost(NULL, NULL, t('Delete'));
-    $this->assertRaw(t('%name has been deleted.', array('%name' => $user->name)), t('User deleted'));
-    $this->assertFalse(user_load($edit), t('User is not found in the database'));
+    $this->drupalPost(NULL, NULL, t('Cancel accounts'));
+    $status = TRUE;
+    foreach ($users as $user) {
+      $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $user->name))) !== FALSE);
+      $status = $status && !user_load($user->uid);
+    }
+    $this->assertTrue($status, t('Users deleted and not found in the database.'));
+    // Ensure that admin account was not cancelled.
+    $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Cancel account request mailed message displayed.'));
+    $this->assertTrue(user_load(array('uid' => $admin_user->uid, 'status' => 1)), t('Administrative user is found in the database and enabled.'));
   }
 }
 
