Commit 9a32ca46 authored by webchick's avatar webchick

#8 by sun and most of #drupal: Users can now cancel their accounts. Fixing the...

#8 by sun and most of #drupal: Users can now cancel their accounts. Fixing the 8th issue, almost 8 years later, on January 8th, after working 8 days full-time on it. GREAT WORK :D
parent 1eb5be73
......@@ -32,6 +32,7 @@ Drupal 7.0, xxxx-xx-xx (development version)
* 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.
......
......@@ -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_reassign':
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;
}
}
/**
......
......@@ -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_reassign':
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() {
......
......@@ -165,7 +165,7 @@ class DBLogTestCase extends DrupalWebTestCase {
$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.
}
......@@ -202,8 +202,13 @@ class DBLogTestCase extends DrupalWebTestCase {
}
$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)));
// Login the admin user.
$this->drupalLogin($this->big_user);
// Delete user.
user_delete(array(), $user->uid);
// We need to POST here to invoke batch_process() in the internal browser.
$this->drupalPost('user/' . $user->uid . '/cancel', array('user_cancel_method' => 'user_cancel_reassign'), t('Cancel account'));
// 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)));
......@@ -225,8 +230,6 @@ class DBLogTestCase extends DrupalWebTestCase {
}
$this->assertTrue(!isset($ids), t('DBLog contains no records for @name', array('@name' => $user->name)));
// Login the admin user.
$this->drupalLogin($this->big_user);
// View the dblog report.
$this->drupalGet('admin/reports/dblog');
$this->assertResponse(200);
......
......@@ -1476,11 +1476,41 @@ 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);
* Implementation of hook_user_cancel().
*/
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_reassign':
// 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;
}
}
/**
......
......@@ -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_reassign':
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;
}
}
......@@ -259,10 +259,15 @@ function profile_user_categories(&$edit, &$user, $category = NULL) {
}
/**
* Implementation of hook_user_delete().
* Implementation of hook_user_cancel().
*/
function profile_user_delete(&$edit, &$user, $category = NULL) {
db_query('DELETE FROM {profile_value} WHERE uid = %d', $user->uid);
function profile_user_cancel(&$edit, &$account, $method) {
switch ($method) {
case 'user_cancel_reassign':
case 'user_cancel_delete':
db_delete('profile_value')->condition('uid', $account->uid)->execute();
break;
}
}
function profile_load_profile(&$user) {
......
......@@ -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_reassign':
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;
}
}
/**
......
......@@ -441,10 +441,15 @@ function trigger_user_update(&$edit, &$account, $category) {
}
/**
* Implementation of hook_user_delete().
*/
function trigger_user_delete(&$edit, &$account, $category) {
_trigger_user('delete', $edit, $account, $category);
* Implementation of hook_user_cancel().
*/
function trigger_user_cancel(&$edit, &$account, $method) {
switch ($method) {
case 'user_cancel_reassign':
case 'user_cancel_delete':
_trigger_user('delete', $edit, $account, $method);
break;
}
}
/**
......
......@@ -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,30 @@ 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.'));
// Account cancellation settings.
module_load_include('inc', 'user', 'user.pages');
$form['cancel'] = array(
'#type' => 'fieldset',
'#title' => t('Account cancellation settings'),
);
$form['cancel']['user_cancel_method'] = array(
'#type' => 'item',
'#title' => t('When cancelling a user account'),
'#description' => t('This default applies to all users who want to cancel their accounts. Users with the %select-cancel-method or %administer-users <a href="@permissions-url">permissions</a> can override this default method.', array('%select-cancel-method' => t('Select method for cancelling account'), '%administer-users' => t('Administer users'), '@permissions-url' => url('admin/user/permissions'))),
);
$form['cancel']['user_cancel_method'] += user_cancel_methods();
foreach (element_children($form['cancel']['user_cancel_method']) as $element) {
// Remove all account cancellation methods that have #access defined, as
// those cannot be configured as default method.
if (isset($form['cancel']['user_cancel_method'][$element]['#access'])) {
$form['cancel']['user_cancel_method'][$element]['#access'] = FALSE;
}
// Remove the description (only displayed on the confirmation form).
else {
unset($form['cancel']['user_cancel_method'][$element]['#description']);
}
}
// User e-mail settings.
$form['email'] = array(
'#type' => 'fieldset',
......@@ -243,7 +267,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 +399,48 @@ function user_admin_settings() {
'#rows' => 3,
);
$form['email']['deleted'] = array(
$form['email']['cancel_confirm'] = array(
'#type' => 'fieldset',
'#title' => t('Account cancellation 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,
);
......
......@@ -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
......@@ -87,6 +85,98 @@ function hook_user($op, &$edit, &$account, $category = NULL) {
}
}
/**
* Act on user account cancellations.
*
* The user account is being canceled. Depending on the account cancellation
* method, the module should either do nothing, unpublish content, anonymize
* content, or delete content and data belonging to the canceled user account.
*
* Expensive operations should be added to the global batch with batch_set().
*
* @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_methods()
* @see hook_user_cancel_methods_alter()
* @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_reassign':
// 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).
$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;
}
}
/**
* Modify account cancellation methods.
*
* By implementing this hook, modules are able to add, customize, or remove
* account cancellation methods. All defined methods are turned into radio
* button form elements by user_cancel_methods() after this hook is invoked.
* The following properties can be defined for each method:
* - title: The radio button's title.
* - description: (optional) A description to display on the confirmation form
* if the user is not allowed to select the account cancellation method. The
* description is NOT used for the radio button, but instead should provide
* additional explanation to the user seeking to cancel their account.
* - access: (optional) A boolean value indicating whether the user can access
* a method. If #access is defined, the method cannot be configured as default
* method.
*
* @param &$methods
* An array containing user account cancellation methods, keyed by method id.
*
* @see user_cancel_methods()
* @see user_cancel_confirm_form()
*/
function hook_user_cancel_methods_alter(&$methods) {
// Limit access to disable account and unpublish content method.
$methods['user_cancel_block_unpublish']['access'] = user_access('administer site configuration');
// Remove the content re-assigning method.
unset($methods['user_cancel_reassign']);
// Add a custom zero-out method.
$methods['mymodule_zero_out'] = array(
'title' => t('Delete the account and remove all content.'),
'description' => t('All your content will be replaced by empty strings.'),
// access should be used for administrative methods only.
'access' => user_access('access zero-out account cancellation method'),
);
}
/**
* Add mass user operations.
*
......@@ -114,8 +204,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;
......
......@@ -359,6 +359,34 @@ function user_update_7002(&$sandbox) {
return $ret;
}
/**
* Update user settings for cancelling user accounts.
*
* Prior to 7.x, users were not able to cancel their accounts. When
* administrators deleted an account, all contents were assigned to uid 0,
* which is the same as the 'user_cancel_reassign' method now.
*/
function user_update_7003() {
$ret = array();
// Set the default account cancellation method.
variable_set('user_cancel_method', 'user_cancel_reassign');
// Re-assign notification setting.
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');
}
// Re-assign "Account deleted" mail strings to "Account canceled" mail.
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.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment