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 16:34:28 -0000 @@ -32,6 +32,7 @@ Drupal 7.0, xxxx-xx-xx (development vers * Redesigned password strength validator. * Redesigned the add content type screen. * Highlight duplicate URL aliases. + * Added configurable ability for users to cancel their own 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 permissions 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 16:41:21 -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 user settings.', 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 and should define the path to redirect to. + * + * @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. * - * @param $edit An array of submitted form values. - * @param $uid The user ID of the user to delete. + * @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'), + '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' => '