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 7 Jan 2009 00:59:52 -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 7 Jan 2009 00:59:52 -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 7 Jan 2009 00:59:52 -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 7 Jan 2009 00:59:52 -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 7 Jan 2009 00:59:52 -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 7 Jan 2009 00:59:52 -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 7 Jan 2009 01:16:01 -0000
@@ -259,10 +259,15 @@ function profile_user_categories(&$edit,
}
/**
- * 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_ANONYMIZE:
+ case USER_CANCEL_DELETE:
+ db_delete('profile_value')->condition('uid', $account->uid)->execute();
+ break;
+ }
}
function profile_load_profile(&$user) {
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 7 Jan 2009 00:59:52 -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.26
diff -u -p -r1.26 trigger.module
--- modules/trigger/trigger.module 7 Jan 2009 03:52:32 -0000 1.26
+++ modules/trigger/trigger.module 7 Jan 2009 03:57:16 -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 7 Jan 2009 17:58:33 -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,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 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'))),
+ );
+ $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'])) {
+ unset($form['cancel']['user_cancel_method'][$element]);
+ }
+ // 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,
);
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 7 Jan 2009 17:53:27 -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,97 @@ function hook_user($op, &$edit, &$accoun
}
/**
+ * 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.
+ *
+ * @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 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).
+ $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 anonymization method.
+ unset($methods[USER_CANCEL_ANONYMIZE]);
+
+ // 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.
*
* This hook enables modules to inject custom operations into the mass operations
@@ -114,8 +203,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 7 Jan 2009 04:57:55 -0000
@@ -360,6 +360,34 @@ function user_update_7002(&$sandbox) {
}
/**
+ * 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_ANONYMIZE method now.
+ */
+function user_update_7003() {
+ $ret = array();
+ // Set the default account cancellation method.
+ variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+ // 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.
*/
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 7 Jan 2009 18:00:41 -0000
@@ -17,6 +17,27 @@ define('USERNAME_MAX_LENGTH', 60);
define('EMAIL_MAX_LENGTH', 64);
/**
+ * Account cancellation method: Disable user account and keep all contents.
+ */
+define('USER_CANCEL_BLOCK', 0);
+
+/**
+ * Account cancellation method: Disable user account and unpublish all contents.
+ */
+define('USER_CANCEL_BLOCK_UNPUBLISH', 1);
+
+/**
+ * Account cancellation method: Remove user account and anonymize all contents.
+ */
+define('USER_CANCEL_ANONYMIZE', 2);
+
+/**
+ * Account cancellation 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 cancellation method' => array(
+ 'title' => t('Select method for cancelling own account'),
+ 'description' => t('Select the method for cancelling own user account. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
+ ),
);
}
@@ -948,6 +977,16 @@ function user_edit_access($account) {
return (($GLOBALS['user']->uid == $account->uid) || user_access('administer users')) && $account->uid > 0;
}
+/**
+ * Menu access callback; limit access to account cancellation pages.
+ *
+ * Limit access to users with the 'cancel account' permission or administrative
+ * users, and prevent the anonymous user from cancelling the account.
+ */
+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 +1121,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 account cancellation',
+ 'page callback' => 'user_cancel_confirm',
+ 'page arguments' => array(1, 4, 5),
+ 'access callback' => 'user_cancel_access',
+ 'access arguments' => array(1),
'type' => MENU_CALLBACK,
);
@@ -1445,6 +1493,17 @@ 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 an account cancellation request.
+ *
+ * @see user_mail_tokens()
+ * @see user_cancel_confirm()
+ */
+function user_cancel_url($account) {
+ $timestamp = REQUEST_TIME;
+ return url("user/$account->uid/cancel/confirm/$timestamp/" . 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 +1654,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 to delete.
+ * @param $edit
+ * An array of submitted form values.
+ * @param $uid
+ * The user ID of the user account to cancel.
+ * @param $method
+ * The account cancellation method to use.
+ *
+ * @see _user_cancel()
*/
-function user_delete($edit, $uid) {
+function user_cancel($edit, $uid, $method) {
+ global $user;
+
$account = user_load(array('uid' => $uid));
- drupal_session_destroy_uid($uid);
- _user_mail_notify('status_deleted', $account);
- module_invoke_all('user_delete', $edit, $account);
- db_query('DELETE FROM {users} WHERE uid = %d', $uid);
- db_query('DELETE FROM {users_roles} WHERE uid = %d', $uid);
- db_query('DELETE FROM {authmap} WHERE uid = %d', $uid);
- $variables = array('%name' => $account->name, '%email' => '<' . $account->mail . '>');
- watchdog('user', 'Deleted user: %name %email.', $variables, WATCHDOG_NOTICE);
+
+ if (!$account) {
+ drupal_set_message(t('The user account %id does not exist.', array('%id' => $uid)), 'error');
+ watchdog('user', 'Attempted to cancel non-existing user account: %id.', array('%id' => $uid), WATCHDOG_ERROR);
+ return;
+ }
+
+ // Initialize batch (to set title).
+ $batch = array(
+ 'title' => t('Cancelling account'),
+ '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 +1826,28 @@ 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('Account cancellation 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
+
+NOTE: The cancellation of your account is not reversible.
+
+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 +1914,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 +2028,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 +2037,64 @@ 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('When cancelling these accounts'),
+ );
+ $form['user_cancel_method'] += user_cancel_methods();
+ // Remove method descriptions.
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+
+ // Allow to send the account cancellation confirmation mail.
+ $form['user_cancel_confirm'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require e-mail confirmation to cancel account.'),
+ '#default_value' => FALSE,
+ '#description' => t('When enabled, the user must confirm the account cancellation via e-mail.'),
+ );
+ // Also allow to send account canceled notification mail, if enabled.
+ $form['user_cancel_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is canceled.'),
+ '#default_value' => FALSE,
+ '#access' => variable_get('user_mail_status_canceled_notify', FALSE),
+ '#description' => t('When enabled, the user will receive an e-mail notification after the account has been cancelled.'),
+ );
return confirm_form($form,
- t('Are you sure you want to delete these users?'),
+ t('Are you sure you want to cancel these user accounts?'),
'admin/user/user', t('This action cannot be undone.'),
- t('Delete all'), t('Cancel'));
+ t('Cancel accounts'), t('Cancel'));
}
-function user_multiple_delete_confirm_submit($form, &$form_state) {
+/**
+ * Submit handler for mass-account cancellation form.
+ *
+ * @see user_multiple_cancel_confirm()
+ * @see user_cancel_confirm_form_submit()
+ */
+function user_multiple_cancel_confirm_submit($form, &$form_state) {
+ global $user;
+
if ($form_state['values']['confirm']) {
foreach ($form_state['values']['accounts'] as $uid => $value) {
- user_delete($form_state['values'], $uid);
+ // Prevent user administrators from deleting themselves without confirmation.
+ if ($uid == $user->uid) {
+ $admin_form_state = $form_state;
+ unset($admin_form_state['values']['user_cancel_confirm']);
+ $admin_form_state['values']['_account'] = $user;
+ user_cancel_confirm_form_submit(array(), $admin_form_state);
+ }
+ else {
+ user_cancel($form_state['values'], $uid, $form_state['values']['user_cancel_method']);
+ }
}
- drupal_set_message(t('The users have been deleted.'));
}
$form_state['redirect'] = 'admin/user/user';
return;
@@ -2082,6 +2288,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,
@@ -2133,7 +2340,8 @@ function user_preferred_language($accoun
* 'password_reset': Password recovery request
* 'status_activated': Account activated
* 'status_blocked': Account blocked
- * 'status_deleted': Account deleted
+ * 'cancel_confirm': Account cancellation request
+ * 'status_canceled': Account canceled
*
* @param $account
* The user object of the account being notified. Must contain at
@@ -2144,8 +2352,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 7 Jan 2009 18:04:58 -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';
@@ -270,7 +271,7 @@ function user_profile_form_validate($for
function user_profile_form_submit($form, &$form_state) {
$account = $form_state['values']['_account'];
$category = $form_state['values']['_category'];
- unset($form_state['values']['_account'], $form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['delete'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['_category'], $form_state['values']['form_build_id']);
+ unset($form_state['values']['_account'], $form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['cancel'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['_category'], $form_state['values']['form_build_id']);
user_module_invoke('submit', $form_state['values'], $account, $category);
user_save($account, $form_state['values'], $category);
@@ -282,45 +283,207 @@ 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 account cancellation method selection, if allowed.
+ $default_method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+ $admin_access = user_access('administer users');
+ $can_select_method = $admin_access || user_access('select account cancellation method');
+ $form['user_cancel_method'] = array(
+ '#type' => 'item',
+ '#title' => ($account->uid == $user->uid ? t('When cancelling your account') : t('When cancelling the account')),
+ '#access' => $can_select_method,
+ );
+ $form['user_cancel_method'] += user_cancel_methods();
+
+ // Allow user administrators to skip the account cancellation confirmation
+ // mail (by default), as long as they do not attempt to cancel their own
+ // account.
+ $override_access = $admin_access && ($account->uid != $user->uid);
+ $form['user_cancel_confirm'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require e-mail confirmation to cancel account.'),
+ '#default_value' => ($override_access ? FALSE : TRUE),
+ '#access' => $override_access,
+ '#description' => t('When enabled, the user must confirm the account cancellation via e-mail.'),
+ );
+ // Also allow to send account canceled notification mail, if enabled.
+ $default_notify = variable_get('user_mail_status_canceled_notify', FALSE);
+ $form['user_cancel_notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user when account is canceled.'),
+ '#default_value' => ($override_access ? FALSE : $default_notify),
+ '#access' => $override_access && $default_notify,
+ '#description' => t('When enabled, the user will receive an e-mail notification after the account has been cancelled.'),
+ );
+
+ // Prepare confirmation form page title and description.
+ if ($account->uid == $user->uid) {
+ $question = t('Are you sure you want to cancel your account?');
+ }
+ else {
+ $question = t('Are you sure you want to cancel the account %name?', array('%name' => $account->name));
+ }
+ if ($can_select_method) {
+ $description = t('Select the method to cancel the account above.');
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+ }
+ else {
+ // The radio button #description is used as description for the confirmation
+ // form.
+ foreach (element_children($form['user_cancel_method']) as $element) {
+ if ($form['user_cancel_method'][$element]['#default_value'] == $form['user_cancel_method'][$element]['#return_value']) {
+ $description = $form['user_cancel_method'][$element]['#description'];
+ }
+ unset($form['user_cancel_method'][$element]['#description']);
+ }
+ }
+
return confirm_form($form,
- t('Are you sure you want to delete the account %name?', array('%name' => $account->name)),
+ $question,
'user/' . $account->uid,
- t('All submissions made by this user will be attributed to the anonymous account. This action cannot be undone.'),
- t('Delete'), t('Cancel'));
+ $description . ' ' . t('This action cannot be undone.'),
+ t('Cancel account'), t('Cancel'));
}
/**
- * Submit function for the confirm form for user deletion.
+ * Submit handler for the account cancellation confirm form.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_multiple_cancel_confirm_submit()
*/
-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'];
- if (!isset($_REQUEST['destination'])) {
- $form_state['redirect'] = 'admin/user/user';
+ // Cancel account immediately, if the current user has administrative
+ // privileges, no confirmation mail shall be sent, and the user does not
+ // attempt to cancel the own account.
+ if (user_access('administer users') && empty($form_state['values']['user_cancel_confirm']) && $account->uid != $user->uid) {
+ user_cancel($form_state['values'], $account->uid, $form_state['values']['user_cancel_method']);
+
+ if (!isset($_REQUEST['destination'])) {
+ $form_state['redirect'] = 'admin/user/user';
+ }
+ }
+ else {
+ // Store cancelling method and whether to notify the user in $account for
+ // user_cancel_confirm().
+ $edit = array(
+ 'user_cancel_method' => $form_state['values']['user_cancel_method'],
+ 'user_cancel_notify' => $form_state['values']['user_cancel_notify'],
+ );
+ $account = user_save($account, $edit);
+ _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 available account cancellation methods.
+ *
+ * Please refer to the documentation of hook_user_cancel_methods_alter().
+ *
+ * @return
+ * An array containing all account cancellation methods as form elements.
+ *
+ * @see hook_user_cancel_methods_alter()
+ * @see user_admin_settings()
+ * @see user_cancel_confirm_form()
+ * @see user_multiple_cancel_confirm()
+ */
+function user_cancel_methods() {
+ $methods = array(
+ USER_CANCEL_BLOCK => array(
+ '#title' => t('Disable the account and keep all content.'),
+ ),
+ USER_CANCEL_BLOCK_UNPUBLISH => array(
+ '#title' => t('Disable the account and unpublish all content.'),
+ '#description' => t('All your content will be unpublished.'),
+ ),
+ USER_CANCEL_ANONYMIZE => array(
+ '#title' => t('Delete the account and make all content belong to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+ '#description' => t('All your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))),
+ ),
+ USER_CANCEL_DELETE => array(
+ '#title' => t('Delete the account and all content.'),
+ '#description' => t('All your content will be deleted.'),
+ '#access' => user_access('administer users'),
+ ),
+ );
+ // Allow modules to customize account cancellation methods.
+ drupal_alter('user_cancel_methods', $methods);
+ // Turn all methods into real form elements.
+ $default_method = variable_get('user_cancel_method', USER_CANCEL_BLOCK);
+ foreach (element_children($methods) as $method) {
+ $item = &$methods[$method];
+ $item['#type'] = 'radio';
+ $item['#return_value'] = $method;
+ $item['#parents'] = array('user_cancel_method');
+ $item['#default_value'] = $default_method;
+ $item['#required'] = TRUE;
+ $item += array('#description' => NULL);
+ }
+ return $methods;
+}
+
+/**
+ * Menu callback; Cancel a user account via e-mail confirmation link.
+ *
+ * @see user_cancel_confirm_form()
+ * @see user_cancel_url()
+ */
+function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
+ // Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
+ $timeout = 86400;
+ $current = REQUEST_TIME;
+
+ // Basic validation of arguments.
+ if (isset($account->user_cancel_method) && !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_notify' => isset($account->user_cancel_notify) ? $account->user_cancel_notify : variable_get('user_mail_status_canceled_notify', FALSE),
+ );
+ user_cancel($edit, $account->uid, $account->user_cancel_method);
+ // Since user_cancel() is not invoked via Form API, batch processing needs
+ // to be invoked manually and should redirect to the front page after
+ // completion.
+ batch_process('');
+ }
+ else {
+ drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'));
+ drupal_goto("user/$account->uid/cancel");
+ }
}
+ drupal_access_denied();
}
function user_edit_validate($form, &$form_state) {
@@ -336,7 +499,7 @@ function user_edit_validate($form, &$for
function user_edit_submit($form, &$form_state) {
$account = $form_state['values']['_account'];
$category = $form_state['values']['_category'];
- unset($form_state['values']['_account'], $form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['delete'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['_category'], $form_state['values']['form_build_id']);
+ unset($form_state['values']['_account'], $form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['cancel'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['_category'], $form_state['values']['form_build_id']);
user_module_invoke('submit', $form_state['values'], $account, $category);
user_save($account, $form_state['values'], $category);
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 7 Jan 2009 13:10:45 -0000
@@ -151,45 +151,333 @@ 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 account cancellation methods work as expected.'),
+ 'group' => t('User'),
);
}
/**
- * Registers a user and deletes it.
+ * Attempt to 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.
+ $account = $this->drupalCreateUser(array());
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
- $user = user_load($edit);
+ // Create a node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
- // Create admin user to delete registered user.
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->assertNoRaw(t('Cancel account'), t('No cancel account button displayed.'));
+
+ // Attempt bogus account cancellation request confirmation.
+ $timestamp = $account->login;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
+ $this->assertResponse(403, t('Bogus cancelling request rejected.'));
+ $this->assertTrue(user_load(array('uid' => $account->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 == $account->uid && $test_node->status == 1), t('Node of the user has not been altered.'));
+ }
+
+ /**
+ * Attempt invalid account cancellations.
+ */
+ function testUserCancelInvalid() {
+ variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
+
+ // Create a node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Attempt to cancel account.
+ $this->drupalPost('user/' . $account->uid . '/edit', NULL, t('Cancel account'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+
+ // Attempt bogus account cancellation request confirmation.
+ $bogus_timestamp = $timestamp + 60;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login));
+ $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), t('Bogus cancelling request rejected.'));
+ $this->assertTrue(user_load(array('uid' => $account->uid, 'status' => 1)), t('User account was not canceled.'));
+
+ // Attempt expired account cancellation request confirmation.
+ $bogus_timestamp = $timestamp - 86400 - 60;
+ $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login));
+ $this->assertText(t('You have tried to use an account cancellation 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' => $account->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 == $account->uid && $test_node->status == 1), t('Node of the user has not been altered.'));
+ }
+
+ /**
+ * Disable account and keep all content.
+ */
+ function testUserBlock() {
+ variable_set('user_cancel_method', USER_CANCEL_BLOCK);
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.'));
+ $this->assertNoText(t('Select the method to cancel the account above.'), t('Does not allow user to select account cancellation method.'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
+ $this->assertTrue(user_load(array('uid' => $account->uid, 'status' => 0)), t('User has been blocked.'));
+
+ // Confirm user is logged out.
+ $this->assertNoText($account->name, t('Logged out.'));
+ }
+
+ /**
+ * Disable account and unpublish all content.
+ */
+ function testUserBlockUnpublish() {
+ variable_set('user_cancel_method', USER_CANCEL_BLOCK_UNPUBLISH);
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
+
+ // Create a node with two revisions.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $settings = get_object_vars($node);
+ $settings['revision'] = 1;
+ $node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.'));
+ $this->assertText(t('All your content will be unpublished.'), t('Informs that all content will be unpublished.'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
+ $this->assertTrue(user_load(array('uid' => $account->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($account->name, t('Logged out.'));
+ }
+
+ /**
+ * Delete account and anonymize all content.
+ */
+ function testUserAnonymize() {
+ variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
+
+ // Create a simple node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Create a node with two revisions, the initial one belonging to the
+ // cancelling user.
+ $revision_node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $revision = $revision_node->vid;
+ $settings = get_object_vars($revision_node);
+ $settings['revision'] = 1;
+ $settings['uid'] = 1; // Set new/current revision to someone else.
+ $revision_node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.'));
+ $this->assertRaw(t('All your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), t('Informs that all content will be attributed to anonymous account.'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
+ $this->assertFalse(user_load($account->uid), t('User is not found in the database.'));
+
+ // Confirm that user's content has been attributed to anonymous user.
+ $test_node = node_load($node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node of the user has been attributed to anonymous user.'));
+ $test_node = node_load($revision_node->nid, $revision, TRUE);
+ $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node revision of the user has been attributed to anonymous user.'));
+ $test_node = node_load($revision_node->nid, NULL, TRUE);
+ $this->assertTrue(($test_node->uid != 0 && $test_node->status == 1), t("Current revision of the user's node was not attributed to anonymous user."));
+
+ // Confirm that user is logged out.
+ $this->assertNoText($account->name, t('Logged out.'));
+ }
+
+ /**
+ * Delete account and remove all content.
+ */
+ function testUserDelete() {
+ variable_set('user_cancel_method', USER_CANCEL_DELETE);
+
+ // Create a user.
+ $account = $this->drupalCreateUser(array('cancel account'));
+ $this->drupalLogin($account);
+ // Load real user object.
+ $account = user_load($account->uid);
+
+ // Create a simple node.
+ $node = $this->drupalCreateNode(array('uid' => $account->uid));
+
+ // Create comment.
+ module_load_include('test', 'comment');
+ $comment = CommentHelperCase::postComment($node, '', $this->randomName(32), TRUE, TRUE);
+ $this->assertTrue(comment_load($comment->id), t('Comment found.'));
+
+ // Create a node with two revisions, the initial one belonging to the
+ // cancelling user.
+ $revision_node = $this->drupalCreateNode(array('uid' => $account->uid));
+ $revision = $revision_node->vid;
+ $settings = get_object_vars($revision_node);
+ $settings['revision'] = 1;
+ $settings['uid'] = 1; // Set new/current revision to someone else.
+ $revision_node = $this->drupalCreateNode($settings);
+
+ // Attempt to cancel account.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.'));
+ $this->assertText(t('All your content will be deleted.'), t('Informs that all content will be deleted.'));
+
+ // Confirm account cancellation.
+ $timestamp = time();
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+
+ // Confirm account cancellation request.
+ $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
+ $this->assertFalse(user_load($account->uid), t('User is not found in the database.'));
+
+ // Confirm that user's content has been deleted.
+ $this->assertFalse(node_load($node->nid, NULL, TRUE), t('Node of the user has been deleted.'));
+ $this->assertFalse(node_load($node->nid, $revision, TRUE), t('Node revision of the user has been deleted.'));
+ $this->assertTrue(node_load($revision_node->nid, NULL, TRUE), t("Current revision of the user's node was not deleted."));
+ $this->assertFalse(comment_load($comment->id), t('Comment of the user has been deleted.'));
+
+ // Confirm that user is logged out.
+ $this->assertNoText($account->name, t('Logged out.'));
+ }
+
+ /**
+ * Create an administrative user and delete another user.
+ */
+ function testUserCancelByAdmin() {
+ variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+
+ // Create a regular user.
+ $account = $this->drupalCreateUser(array());
+
+ // Create administrative user.
$admin_user = $this->drupalCreateUser(array('administer users'));
$this->drupalLogin($admin_user);
- // Delete 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.'));
+ // Delete regular user.
+ $this->drupalGet('user/' . $account->uid . '/edit');
+ $this->drupalPost(NULL, NULL, t('Cancel account'));
+ $this->assertRaw(t('Are you sure you want to cancel the account %name?', array('%name' => $account->name)), t('Confirmation form to cancel account displayed.'));
+ $this->assertText(t('Select the method to cancel the account above.'), t('Allows to select account cancellation 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('Cancel account'));
+ $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), t('User deleted.'));
+ $this->assertFalse(user_load($account->uid), t('User is not found in the database.'));
+ }
+
+ /**
+ * Create an administrative user and mass-delete other users.
+ */
+ function testMassUserCancelByAdmin() {
+ variable_set('user_cancel_method', USER_CANCEL_ANONYMIZE);
+ // Enable account cancellation notification.
+ variable_set('user_mail_status_canceled_notify', TRUE);
+
+ // Create administrative user.
+ $admin_user = $this->drupalCreateUser(array('administer users'));
+ $this->drupalLogin($admin_user);
+
+ // Create some users.
+ $users = array();
+ for ($i = 0; $i < 3; $i++) {
+ $account = $this->drupalCreateUser(array());
+ $users[$account->uid] = $account;
+ }
+
+ // Cancel user accounts, including own one.
+ $edit = array();
+ $edit['operation'] = 'cancel';
+ foreach ($users as $uid => $account) {
+ $edit['accounts['. $uid .']'] = TRUE;
+ }
+ $edit['accounts['. $admin_user->uid .']'] = TRUE;
+ $this->drupalPost('admin/user/user', $edit, t('Update'));
+ $this->assertText(t('Are you sure you want to cancel these user accounts?'), t('Confirmation form to cancel accounts displayed.'));
+ $this->assertText(t('When cancelling these accounts'), t('Allows to select account cancellation method.'));
+ $this->assertText(t('Require e-mail confirmation to cancel account.'), t('Allows to send confirmation mail.'));
+ $this->assertText(t('Notify user when account is canceled.'), t('Allows to send notification mail.'));
+
+ // Confirm deletion.
+ $this->drupalPost(NULL, NULL, t('Cancel accounts'));
+ $status = TRUE;
+ foreach ($users as $account) {
+ $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->name))) !== FALSE);
+ $status = $status && !user_load($account->uid);
+ }
+ $this->assertTrue($status, t('Users deleted and not found in the database.'));
+
+ // Ensure that admin account was not cancelled.
+ $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.'));
+ $this->assertTrue(user_load(array('uid' => $admin_user->uid, 'status' => 1)), t('Administrative user is found in the database and enabled.'));
}
}