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 01:17:08 -0000 @@ -1478,9 +1478,24 @@ function node_ranking() { /** * Implementation of hook_user_delete(). */ -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_delete(&$edit, &$account, $method) { + switch ($method) { + case USER_REMOVAL_BLOCK_UNPUBLISH: + db_update('node')->fields(array('status' => 0))->condition('uid', $account->uid)->execute(); + break; + + case USER_REMOVAL_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_REMOVAL_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/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 2 Jan 2009 02:29:40 -0000 @@ -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.')); + // User removal settings. + $form['removal'] = array( + '#type' => 'fieldset', + '#title' => t('User removal settings'), + ); + $form['removal']['user_removal_method'] = array( + '#type' => 'radios', + '#title' => t('Default account removal method'), + '#default_value' => variable_get('user_removal_method', USER_REMOVAL_BLOCK), + '#options' => user_removal_methods(), + '#description' => t('The default user account removal method applies to all users who choose to remove their accounts. Laws for certain countries, services, and audiences may require one of the listed options. Users with the %select-removal-method or %administer-users permissions are able to override the default method.', array('%select-removal-method' => t('Select removal method for own account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/user/permissions'))), + ); + // 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, !remove_url.'; $form['email']['admin_created'] = array( '#type' => 'fieldset', @@ -375,6 +388,26 @@ function user_admin_settings() { '#rows' => 3, ); + $form['email']['removal'] = array( + '#type' => 'fieldset', + '#title' => t('Account removal email'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Customize e-mail messages sent to users when they attempt to remove their accounts.') . ' ' . $email_token_help, + ); + $form['email']['removal']['user_mail_removal_subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => _user_mail_text('removal_subject'), + '#maxlength' => 180, + ); + $form['email']['removal']['user_mail_removal_body'] = array( + '#type' => 'textarea', + '#title' => t('Body'), + '#default_value' => _user_mail_text('removal_body'), + '#rows' => 3, + ); + $form['email']['deleted'] = array( '#type' => 'fieldset', '#title' => t('Account deleted email'), @@ -486,6 +519,66 @@ function user_admin_settings() { } /** + * Helper function to return user account removal methods. + * + * @param $account + * (optional) The user account to remove. + * @param $method + * (optional) The account removal method to use. + * @return + * If no argument is passed in, all account removal options 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_confirm_delete() + */ +function user_removal_methods($account = NULL, $method = NULL) { + global $user; + + if (!isset($account)) { + return array( + USER_REMOVAL_BLOCK => t('Disable user account and keep all contents.'), + USER_REMOVAL_BLOCK_UNPUBLISH => t('Disable user account and unpublish all contents.'), + USER_REMOVAL_ANONYMIZE => t('Remove user account and anonymize all contents.'), + USER_REMOVAL_DELETE => t('Remove user account and remove all contents.'), + ); + } + if (!isset($method)) { + return array( + 'question' => ($account->uid == $user->uid ? t('Are you sure you want to remove your account?') : t('Are you sure you want to remove the account %name?', array('%name' => $account->name))), + 'description' => t('Please select the account removal method above.'), + ); + } + switch ($method) { + case USER_REMOVAL_BLOCK: + default: + return array( + 'question' => ($account->uid == $user->uid ? t('Are you sure you want to disable your account?') : t('Are you sure you want to disable the account %name?', array('%name' => $account->name))), + 'description' => '', + ); + + case USER_REMOVAL_BLOCK_UNPUBLISH: + return array( + 'question' => ($account->uid == $user->uid ? t('Are you sure you want to disable your account?') : t('Are you sure you want to disable the account %name?', array('%name' => $account->name))), + 'description' => ($account->uid == $user->uid ? t('All your submissions will be unpublished.') : t('All submissions made by this user will be unpublished.')), + ); + + case USER_REMOVAL_ANONYMIZE: + return array( + 'question' => ($account->uid == $user->uid ? t('Are you sure you want to remove your account?') : t('Are you sure you want to remove the account %name?', array('%name' => $account->name))), + '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_REMOVAL_DELETE: + return array( + 'question' => ($account->uid == $user->uid ? t('Are you sure you want to remove your account?') : t('Are you sure you want to remove the account %name?', array('%name' => $account->name))), + 'description' => ($account->uid == $user->uid ? t('All your submissions will be deleted.') : t('All submissions made by this user will be deleted.')), + ); + } +} + +/** * Menu callback: administer permissions. * * @ingroup forms 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 1 Jan 2009 21:04:19 -0000 @@ -360,6 +360,19 @@ function user_update_7002(&$sandbox) { } /** + * Set default method for removing user accounts. + * + * Prior 7.x, users were not able to remove 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.admin.inc'; + variable_set('user_removal_method', USER_REMOVAL_ANONYMIZE); + 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 2 Jan 2009 04:50:13 -0000 @@ -17,6 +17,27 @@ define('USERNAME_MAX_LENGTH', 60); define('EMAIL_MAX_LENGTH', 64); /** + * User removal method: Disable user account and keep all contents. + */ +define('USER_REMOVAL_BLOCK', 0); + +/** + * User removal method: Disable user account and unpublish all contents. + */ +define('USER_REMOVAL_BLOCK_UNPUBLISH', 1); + +/** + * User removal method: Remove user account and anonymize all contents. + */ +define('USER_REMOVAL_ANONYMIZE', 2); + +/** + * User removal method: Remove user account and remove all contents. + */ +define('USER_REMOVAL_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.'), ), + 'remove own account' => array( + 'title' => t('Remove or disable own account'), + 'description' => t('Remove or disable own user account and anonymize, unpublish or remove own submissions depending on the configured user settings.', array('@user-settings-url' => url('admin/user/settings'))), + ), + 'select account removal method' => array( + 'title' => t('Select removal method for own account'), + 'description' => t('Select the method for removing 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 user delete/remove pages. + */ +function user_delete_access($account) { + return ((($GLOBALS['user']->uid == $account->uid) && user_access('remove own account')) || user_access('administer users')) && $account->uid > 0; +} + function user_load_self($arg) { $arg[1] = user_load($GLOBALS['user']->uid); return $arg; @@ -1086,8 +1122,17 @@ function user_menu() { 'title' => 'Delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('user_confirm_delete', 1), - 'access callback' => 'user_access', - 'access arguments' => array('administer users'), + 'access callback' => 'user_delete_access', + 'access arguments' => array(1), + 'type' => MENU_CALLBACK, + ); + + $items['user/%user/remove'] = array( + 'title' => 'Account removal', + 'page callback' => 'user_remove', + 'page arguments' => array(1, 3, 4, 5), + 'access callback' => 'user_delete_access', + 'access arguments' => array(1), 'type' => MENU_CALLBACK, ); @@ -1445,6 +1490,19 @@ 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 user account removal. + * + * @see user_mail_tokens() + */ +function user_remove_url($account) { + if (!isset($account->user_removal_method)) { + $account->user_removal_method = variable_get('user_removal_method', USER_REMOVAL_BLOCK); + } + $timestamp = REQUEST_TIME; + return url("user/$account->uid/remove/$timestamp/{$account->user_removal_method}/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); +} + function user_pass_rehash($password, $timestamp, $login) { return md5($timestamp . $password . $login); } @@ -1597,19 +1655,48 @@ function _user_edit_submit($account, &$e /** * Delete a user. * - * @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 to delete. + * @param $method + * The account removal method to use. */ -function user_delete($edit, $uid) { +function user_delete($edit, $uid, $method) { $account = user_load(array('uid' => $uid)); + + if (!$account) { + drupal_set_message(t('The user id %id does not exist.', array('%id' => $uid)), 'error'); + watchdog('user', 'Attempted to delete non-existing user id: %id.', array('%id' => $uid), WATCHDOG_ERROR); + return; + } + 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); + // @todo hook_user_delete is misleading for USER_REMOVAL_BLOCK* cases. + module_invoke_all('user_delete', $edit, $account, $method); + + switch ($method) { + case USER_REMOVAL_BLOCK: + case USER_REMOVAL_BLOCK_UNPUBLISH: + default: + 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_REMOVAL_ANONYMIZE: + case USER_REMOVAL_DELETE: + // Do not send account deleted notification if option was checked. + if (empty($edit['user_skip_mails'])) { + _user_mail_notify('status_deleted', $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,6 +1769,20 @@ 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 'removal_subject': + return t('Account details for !username at !site (deleted)', $variables, $langcode); + case 'removal_body': + return t("!username, + +A request to remove your account has been made at !site. + +You may now remove your account on !uri_brief by clicking this link or copying and pasting it into your browser: + +!remove_url + +This link expires in one day and nothing will happen if it is not used.", $variables, $langcode); + case 'status_deleted_subject': return t('Account details for !username at !site (deleted)', $variables, $langcode); case 'status_deleted_body': @@ -1875,7 +1976,19 @@ 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' => '