Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.678 diff -u -p -r1.678 comment.module --- modules/comment/comment.module 2 Jan 2009 21:17:35 -0000 1.678 +++ modules/comment/comment.module 4 Jan 2009 00:53:41 -0000 @@ -697,17 +697,25 @@ 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: + db_delete('comment')->condition('uid', $account->uid)->execute(); + db_delete('node_comment_statistics')->condition('last_comment_uid', $account->uid)->execute(); + 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.1008 diff -u -p -r1.1008 node.module --- modules/node/node.module 31 Dec 2008 12:02:22 -0000 1.1008 +++ modules/node/node.module 2 Jan 2009 23:49:44 -0000 @@ -1476,11 +1476,26 @@ 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: + db_update('node')->fields(array('status' => 0))->condition('uid', $account->uid)->execute(); + break; + + case USER_CANCEL_ANONYMIZE: + db_update('node')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute(); + db_update('node_revision')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute(); + db_delete('history')->condition('uid', $account->uid)->execute(); + break; + + case USER_CANCEL_DELETE: + db_delete('node')->condition('uid', $account->uid)->execute(); + db_delete('node_revision')->condition('uid', $account->uid)->execute(); + 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.24 diff -u -p -r1.24 trigger.module --- modules/trigger/trigger.module 18 Dec 2008 23:07:50 -0000 1.24 +++ modules/trigger/trigger.module 3 Jan 2009 23:42:56 -0000 @@ -411,10 +411,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 4 Jan 2009 04:25:52 -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,19 @@ 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' => 'item', + '#title' => t('Default cancel account method'), + '#description' => t('The default cancel account method applies to all users who choose to cancel their accounts. Please note that laws for certain countries, services, and audiences may require one of the listed options. Users with the %select-cancel-method or %administer-users permissions are able to override the default method.', array('%select-cancel-method' => t('Select method for cancelling account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/user/permissions'))), + ); + $form['cancel']['user_cancel_method'] += user_cancel_methods(); + // User e-mail settings. $form['email'] = array( '#type' => 'fieldset', @@ -243,7 +256,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 +388,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 4 Jan 2009 00:03:48 -0000 @@ -23,8 +23,10 @@ * (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. + * - "cancel": 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 related to the canceled + * user account. * - "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 @@ -114,8 +116,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.951 diff -u -p -r1.951 user.module --- modules/user/user.module 30 Dec 2008 16:43:20 -0000 1.951 +++ modules/user/user.module 4 Jan 2009 04:26:03 -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,64 @@ function _user_edit_submit($account, &$e } /** - * Delete a user. + * Cancel a user account. * - * @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. */ -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; + } + + if ($account->uid == $user->uid) { + // Destroy the current session. + session_destroy(); + // Load the anonymous user. + $user = drupal_anonymous_user(); + } + else { + drupal_session_destroy_uid($uid); + } + + module_invoke_all('user_cancel', $edit, $account, $method); + + 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', $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', $uid)->execute(); + db_delete('users_roles')->condition('uid', $uid)->execute(); + db_delete('authmap')->condition('uid', $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; + } } /** @@ -1682,10 +1787,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 +1873,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 +1987,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' => '', '#tree' => TRUE); @@ -1875,20 +1996,55 @@ 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' => '
  • ', '#suffix' => check_plain($user) . "
  • \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' => 'item', + '#title' => t('Cancel account method'), + ); + $form['user_cancel_method'] += user_cancel_methods(); + + // 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. + 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.'), + ); + } 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) { +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; @@ -2085,6 +2241,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, @@ -2147,8 +2304,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 4 Jan 2009 05:54:31 -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,44 +283,230 @@ 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'] = array( + '#type' => 'item', + '#title' => t('Cancel account method'), + ); + $form['user_cancel'] += user_cancel_methods(); + } + 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('Confirm'), t('Cancel')); } /** * Submit function for the confirm form for user deletion. */ -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_confirm_form_submit($form, &$form_state) { + global $user; + $account = $form_state['values']['_account']; + + // Cancel account immediately, if the current user has administrative + // privileges and no confirmation mail shall be sent. + if (user_access('administer users') && empty($form_state['values']['user_cancel_confirm'])) { + 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_cancel_methods($account = NULL, $method = NULL) { + global $user; + + if (!isset($account)) { + $default_method = variable_get('user_cancel_method', USER_CANCEL_BLOCK); + $parents = array('user_cancel_method'); + return array( + USER_CANCEL_BLOCK => array( + '#type' => 'radio', + '#name' => 'user_cancel_method', + '#required' => TRUE, + '#default_value' => $default_method, + '#parents' => $parents, + '#return_value' => USER_CANCEL_BLOCK, + '#title' => t('Disable user account and keep all content.'), + '#description' => t('The user account will be disabled. All content will be kept as is.'), + ), + USER_CANCEL_BLOCK_UNPUBLISH => array( + '#type' => 'radio', + '#name' => 'user_cancel_method', + '#required' => TRUE, + '#default_value' => $default_method, + '#parents' => $parents, + '#return_value' => USER_CANCEL_BLOCK_UNPUBLISH, + '#title' => t('Disable user account and unpublish all content.'), + '#description' => t('The user account will be disabled. All content will be unpublished and no longer visible to other users.'), + ), + USER_CANCEL_ANONYMIZE => array( + '#type' => 'radio', + '#name' => 'user_cancel_method', + '#required' => TRUE, + '#default_value' => $default_method, + '#parents' => $parents, + '#return_value' => USER_CANCEL_ANONYMIZE, + '#title' => t('Remove user account and anonymize all content.'), + '#description' => t('The user account will be deleted. All content will be attributed to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), + ), + USER_CANCEL_DELETE => array( + '#type' => 'radio', + '#name' => 'user_cancel_method', + '#required' => TRUE, + '#default_value' => $default_method, + '#parents' => $parents, + '#return_value' => USER_CANCEL_DELETE, + '#title' => t('Remove user account and remove all content.'), + '#description' => t('The user account will be deleted. All content will be deleted.'), + ), + ); + } + $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 (!isset($method)) { + return array( + 'question' => $question, + 'description' => t('Please select the method to cancel the account above.'), + ); + } + switch ($method) { + case USER_CANCEL_BLOCK: + default: + return array('question' => $question, 'description' => ''); + + case USER_CANCEL_BLOCK_UNPUBLISH: + return array( + 'question' => $question, + 'description' => ($account->uid == $user->uid ? t('All your submissions will be unpublished.') : t('All submissions made by this user will be unpublished.')), + ); + + case USER_CANCEL_ANONYMIZE: + return array( + 'question' => $question, + 'description' => ($account->uid == $user->uid ? t('All your submissions will be anonymized.') : t('All submissions made by this user will be attributed to the anonymous account.')), + ); + + case USER_CANCEL_DELETE: + return array( + 'question' => $question, + 'description' => ($account->uid == $user->uid ? t('All your submissions will be deleted.') : t('All submissions made by this user 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); + 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"); + } + } + else { + drupal_access_denied(); } } 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 3 Jan 2009 23:09:03 -0000 @@ -151,45 +151,259 @@ 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 (@see #353618). + $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 delete user. + $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 (@see #353618). + $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 request to cancel account. + $this->drupalPost(NULL, NULL, t('Confirm')); + $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 (@see #353618). + $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 submissions will be unpublished. This action cannot be undone.'), t('[confirm deletion] Inform that all submissions will be unpublished.')); + + // Confirm request to cancel account. + $this->drupalPost(NULL, NULL, t('Confirm')); + $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 (@see #353618). + $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 delete user. + $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 submissions will be anonymized. This action cannot be undone.'), t('[confirm deletion] Inform that all submissions will be attributed to anonymous account.')); + + // Confirm request to cancel account. + $this->drupalPost(NULL, NULL, t('Confirm')); + $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($node->nid, $node->vid, TRUE); + $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node revision of the user has been 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 (@see #353618). + $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 delete user. + $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 submissions will be deleted. This action cannot be undone.'), t('[confirm deletion] Inform that all submissions will be deleted.')); + + // Confirm request to cancel account. + $this->drupalPost(NULL, NULL, t('Confirm')); + $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 attributed to anonymous user. + $this->assertFalse(node_load($node->nid, NULL, TRUE), t('Node of the user has been deleted.')); + $this->assertFalse(node_load($node->nid, $node->vid, TRUE), t('Node revision 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() { + // Set cancel account method to "Remove user account and anonymize all contents." + 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('Please 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('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('Confirm')); + $this->assertRaw(t('%name has been deleted.', array('%name' => $user->name)), t('User deleted.')); + $this->assertFalse(user_load(array('uid' => $user->uid)), t('User is not found in the database.')); } }