You are here

user_delete.module in User Delete 5

Same filename and directory in other branches
  1. 6.2 user_delete.module
  2. 6 user_delete.module

User delete - Let users delete their own account.

This module will be abandoned when http://drupal.org/node/8 is fixed.

Note: As of January 8, 2009 - 09:44 the issue is marked as fixed. And commited to Drupal 7, see http://drupal.org/node/8#comment-1188824

@author Stefan Auditor <stefan.auditor@erdfisch.de>

TODO: Double check comment backup function Allow to admin defined a default setting for normal users Integrate with batchapi

File

user_delete.module
View source
<?php

/**
 * @file
 * User delete - Let users delete their own account.
 *
 * This module will be abandoned when http://drupal.org/node/8 is fixed.
 *
 * Note: As of January 8, 2009 - 09:44 the issue is marked as fixed.
 * And commited to Drupal 7, see http://drupal.org/node/8#comment-1188824
 *
 * @author
 * Stefan Auditor <stefan.auditor@erdfisch.de>
 *
 * TODO:
 * Double check comment backup function
 * Allow to admin defined a default setting for normal users
 * Integrate with batchapi
 */
define('USER_DELETE_FILE_PATH', file_directory_path() . '/user_delete_backup');

/**
 * Implementation of hook_perm().
 */
function user_delete_perm() {
  return array(
    'delete own account',
  );
}

/**
 * Implementation of hook_menu().
 */
function user_delete_menu($may_cache) {
  global $user;
  $items = array();
  if (!$may_cache) {
    $items[] = array(
      'path' => 'user/' . arg(1) . '/delete',
      'title' => t('Delete'),
      'callback' => 'user_edit',
      'access' => user_access('administer users') || user_access('delete own account') && arg(1) == $user->uid,
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/user/user_delete',
      'title' => t('User delete'),
      'description' => t("Configure the user delete action."),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'user_delete_settings',
      'access' => user_access('administer users'),
    );
  }
  return $items;
}

/**
 * Implementation of hook_form_alter().
 */
function user_delete_form_alter($form_id, &$form) {
  global $user;
  if ($form_id == 'user_edit') {

    //access check
    if (user_access('delete own account') && $form['_account']['#value']->uid == $user->uid) {
      $form['delete'] = array(
        '#type' => 'submit',
        '#value' => t('Delete'),
        '#weight' => 31,
      );
    }
  }
  if ($form_id == 'user_confirm_delete') {
    $description = '';
    $default_op = variable_get('user_delete_default_action', 0);
    if ($default_op) {
      switch ($default_op) {
        case 'user_delete_block':
          $description = t('The account will be disabled, all submitted content will be kept.');
          break;
        case 'user_delete_block_unpublish':
          $description = t('The account will be disabled, all submitted content will be unpublished.');
          break;
        case 'user_delete_reassign':
          $description = t('The account will be deleted, all submitted content will be reassigned to the <em>Anonymous user</em>. This action cannot be undone.');
          break;
        case 'user_delete_delete':
          $description = t('The account and all submitted content will be deleted. This action cannot be undone.');
          break;
      }
      $form['description'] = array(
        '#value' => $description,
        '#weight' => -10,
      );
    }
    if (variable_get('user_delete_backup', 0)) {
      $form['user_delete_remark'] = array(
        '#value' => t('All data that is being deleted will be backed up for %period and automatically deleted afterwards.', array(
          '%period' => format_interval(variable_get('user_delete_backup_period', 60 * 60 * 24 * 7 * 12), 2),
        )),
        '#weight' => 0,
      );
    }
    if (!variable_get('user_delete_default_action', 0)) {
      $form['user_delete_action'] = array(
        '#type' => 'radios',
        '#title' => t('When deleting the account'),
        '#default_value' => 'user_delete_block',
        '#options' => array(
          'user_delete_block' => t('Disable the account and keep all content.'),
          'user_delete_block_unpublish' => t('Disable the account and unpublish all content.'),
          'user_delete_reassign' => t('Delete the account and make all content belong to the <em>Anonymous user</em>.'),
          'user_delete_delete' => t('Delete the account and all content.'),
        ),
        '#weight' => 0,
      );
    }
    $form['#redirect'] = 'user/' . $form['uid']['#value'];
    $form['#submit'] = array(
      'user_delete_submit' => array(),
    );
  }
}

/**
 * Deal with the user/content after form submission
 */
function user_delete_submit($form_id, $form_values) {
  global $user;
  $default_op = variable_get('user_delete_default_action', 0);
  $op = $default_op ? $default_op : $form_values['user_delete_action'];
  $uid = $form_values['uid'];
  $account = user_load(array(
    'uid' => $uid,
  ));
  $backup = variable_get('user_delete_backup', 0);
  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;
  }
  switch ($op) {
    case 'user_delete_block':

      // block user
      db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
      drupal_set_message(t('%name has been blocked.', array(
        '%name' => check_plain($account->name),
      )));
      break;
    case 'user_delete_block_unpublish':

      // block user
      db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);

      // unpublish content
      db_query("UPDATE {node} SET status = 0 WHERE uid = %d", $uid);
      db_query("UPDATE {comments} SET status = 1 WHERE uid = %d", $uid);
      drupal_set_message(t('%name has been blocked, all submitted content from that user has been unpublished.', array(
        '%name' => check_plain($account->name),
      )));
      break;
    case 'user_delete_reassign':

      // Set redirect
      $redirect = variable_get('user_delete_redirect', 'node');

      // delete account
      user_delete($form_values, $uid);
      drupal_set_message(t('User record deleted. All submitted content from %name has been reassigned to %anonymous.', array(
        '%name' => check_plain($account->name),
        '%anonymous' => variable_get('anonymous', t('Anonymous')),
      )));
      break;
    case 'user_delete_delete':

      // TODO: Deleting/Backing-up nodes and comments should be done with
      // http://drupal.org/project/batchapi
      // Set redirect
      $redirect = variable_get('user_delete_redirect', 'node');

      // delete comments
      $result = db_query("SELECT cid FROM {comments} WHERE uid = %d", $uid);
      while ($row = db_fetch_object($result)) {

        // backup
        if ($backup) {
          $comment = _comment_load($row->cid);
          user_delete_backup($account, $comment);
        }
        user_delete_comment_delete($row->cid);
      }

      // delete nodes
      $result = db_query("SELECT nid FROM {node} WHERE uid = %d", $uid);
      while ($row = db_fetch_object($result)) {

        // backup
        if ($backup) {
          $node = node_load($row->nid);
          user_delete_backup($account, $node);
        }
        user_delete_node_delete($row->nid);
      }

      // backup
      if ($backup) {
        user_delete_backup($account);
      }

      // delete user
      user_delete($form_values, $uid);
      drupal_set_message(t('User record deleted. All submitted content from %name has been deleted.', array(
        '%name' => check_plain($account->name),
        '!anonymous' => variable_get('anonymous', t('Anonymous')),
      )));
      break;
  }

  // After cancelling account, ensure that user is logged out.
  // Destroy the current session.
  db_query("DELETE FROM {sessions} WHERE uid = %d", $account->uid);
  if ($account->uid == $user->uid) {

    // Load the anonymous user.
    $user = drupal_anonymous_user();

    // Set redirect
    $redirect = variable_get('user_delete_redirect', 'node');
  }

  // Clear the cache for anonymous users.
  cache_clear_all();

  // Redirect
  if (!empty($redirect)) {
    drupal_goto($redirect);
  }
}

/**
 * Implementation of hook_cron().
 */
function user_delete_cron() {
  user_delete_backup_scan_expired();
}

/**
 * Backup an user/node/comment object to the filesystem
 */
function user_delete_backup($account, $object = NULL) {

  // check for directory
  $dir = USER_DELETE_FILE_PATH;
  user_delete_file_check_directory($dir, TRUE);
  file_check_directory($dir, TRUE);
  $backup_dir = $dir . '/' . check_plain($account->name);
  user_delete_file_check_directory($backup_dir, TRUE);
  if (is_numeric($object->cid)) {
    $dest = $backup_dir . '/comments';
    user_delete_file_check_directory($dest, TRUE);
    $dest = $dest . '/comment-' . $object->cid . '.txt';
  }
  else {
    if (is_numeric($object->nid)) {
      $dest = $backup_dir . '/nodes';
      user_delete_file_check_directory($dest, TRUE);
      $dest = $dest . '/node-' . $object->nid . '.txt';
    }
    else {
      $dest = $backup_dir;
      $object = $account;
      user_delete_file_check_directory($dest, TRUE);
      $dest = $dest . '/user-' . $object->uid . '.txt';
    }
  }
  $data = serialize((array) $object);
  file_save_data($data, $dest, FILE_EXISTS_REPLACE);
}

/**
 * Scan for and delete expired files
 */
function user_delete_backup_scan_expired() {

  // check for directory
  $dir = USER_DELETE_FILE_PATH;
  if (file_check_directory($dir, TRUE)) {
    file_scan_directory($dir, '.*', array(
      '.',
      '..',
      'CVS',
    ), 'user_delete_backup_remove_expired', FALSE);
  }
}

/**
 * Check if a folder is expired and delete
 */
function user_delete_backup_remove_expired($filename) {
  $period = variable_get('user_delete_backup_period', 60 * 60 * 24 * 7 * 12);
  $created = filemtime($filename);
  if ($created && time() >= $created + $period) {
    user_delete_backup_remove_dir($filename);
  }
}

/**
 * Recursive delete a folder with files
 */
function user_delete_backup_remove_dir($dir) {
  if (!file_exists($dir)) {
    return TRUE;
  }
  if (!is_dir($dir)) {
    return unlink($dir);
  }
  foreach (scandir($dir) as $item) {
    if ($item == '.' || $item == '..') {
      continue;
    }
    if (!user_delete_backup_remove_dir($dir . DIRECTORY_SEPARATOR . $item)) {
      return FALSE;
    }
  }
  return rmdir($dir);
}

/**
 * Copy of node_delete() whithout access check and drupal_set_message().
 * see http://api.drupal.org/api/function/node_delete/5
 */
function user_delete_node_delete($nid) {
  $node = node_load($nid);
  db_query('DELETE FROM {node} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid);

  // Call the node-specific callback (if any):
  node_invoke($node, 'delete');
  node_invoke_nodeapi($node, 'delete');

  // Clear the cache so an anonymous poster can see the node being deleted.
  cache_clear_all();

  // Remove this node from the search index if needed.
  if (function_exists('search_wipe')) {
    search_wipe($node->nid, 'node');
  }

  //drupal_set_message(t('%title has been deleted.', array('%title' => $node->title)));
  watchdog('content', t('@type: deleted %title.', array(
    '@type' => t($node->type),
    '%title' => $node->title,
  )));
}

/**
 * Copy of file_check_directory() without drupal_set_message().
 * see http://api.drupal.org/api/function/file_check_directory/5
 */
function user_delete_file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
  $directory = rtrim($directory, '/\\');

  // Check if directory exists.
  if (!is_dir($directory)) {
    if ($mode & FILE_CREATE_DIRECTORY && @mkdir($directory)) {

      //drupal_set_message(t('The directory %directory has been created.', array('%directory' => $directory)));
      @chmod($directory, 0775);

      // Necessary for non-webserver users.
    }
    else {
      if ($form_item) {
        form_set_error($form_item, t('The directory %directory does not exist.', array(
          '%directory' => $directory,
        )));
      }
      return FALSE;
    }
  }

  // Check to see if the directory is writable.
  if (!is_writable($directory)) {
    if ($mode & FILE_MODIFY_PERMISSIONS && @chmod($directory, 0775)) {

      //drupal_set_message(t('The permissions of directory %directory have been changed to make it writable.', array('%directory' => $directory)));
    }
    else {
      form_set_error($form_item, t('The directory %directory is not writable', array(
        '%directory' => $directory,
      )));
      watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array(
        '%directory' => $directory,
      ), WATCHDOG_ERROR);
      return FALSE;
    }
  }
  if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("{$directory}/.htaccess")) {
    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
    if (($fp = fopen("{$directory}/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) {
      fclose($fp);
      chmod($directory . '/.htaccess', 0664);
    }
    else {
      $variables = array(
        '%directory' => $directory,
        '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)),
      );
      form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables));
      watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
    }
  }
  return TRUE;
}

/**
 * Administrative settings page
 *
 * @return array
 *   a form array
 */
function user_delete_settings() {

  //TODO: add additional settings based on http://drupal.org/node/8#comment-628434
  $form['defaults'] = array(
    '#type' => 'fieldset',
    '#title' => t('Defaults'),
  );
  $form['defaults']['user_delete_default_action'] = array(
    '#type' => 'select',
    '#title' => t('Default action when deleting'),
    '#default_value' => variable_get('user_delete_default_action', 0),
    '#options' => array(
      0 => t('Let users choose action'),
      'user_delete_block' => t('Disable the account and keep all content.'),
      'user_delete_block_unpublish' => t('Disable the account and unpublish all content.'),
      'user_delete_reassign' => t('Delete the account and make all content belong to the Anonymous user.'),
      'user_delete_delete' => t('Delete the account and all content.'),
    ),
  );
  $form['redirect'] = array(
    '#type' => 'fieldset',
    '#title' => t('Redirect'),
  );
  $form['redirect']['user_delete_redirect'] = array(
    '#type' => 'textfield',
    '#title' => t('Redirection page'),
    '#default_value' => variable_get('user_delete_redirect', 'node'),
    '#description' => t('Choose where to redirect your users after account deletion. Any valid Drupal path will do, e.g. %front or %node', array(
      '%front' => '<front>',
      '%node' => 'node/1',
    )),
  );
  $form['backup'] = array(
    '#type' => 'fieldset',
    '#title' => t('Backup'),
  );
  $form['backup']['user_delete_backup'] = array(
    '#type' => 'checkbox',
    '#title' => t('Backup data'),
    '#default_value' => variable_get('user_delete_backup', 0),
    '#description' => t('Backup data that is being deleted to the filesystem.'),
  );
  $options = array(
    60 * 60 * 24 * 7 => format_interval(60 * 60 * 24 * 7, 2),
    60 * 60 * 24 * 7 * 2 => format_interval(60 * 60 * 24 * 7 * 2, 2),
    60 * 60 * 24 * 7 * 4 => format_interval(60 * 60 * 24 * 7 * 4, 2),
    60 * 60 * 24 * 7 * 8 => format_interval(60 * 60 * 24 * 7 * 8, 2),
    60 * 60 * 24 * 7 * 12 => format_interval(60 * 60 * 24 * 7 * 12, 2),
    60 * 60 * 24 * 7 * 16 => format_interval(60 * 60 * 24 * 7 * 16, 2),
    60 * 60 * 24 * 7 * 24 => format_interval(60 * 60 * 24 * 7 * 24, 2),
  );
  $form['backup']['user_delete_backup_period'] = array(
    '#type' => 'select',
    '#title' => t('Keep backup time'),
    '#default_value' => variable_get('user_delete_backup_period', 60 * 60 * 24 * 7 * 12),
    '#options' => $options,
    '#description' => t('The time frame after which the backup should be deleted from the filesystem.'),
  );
  return system_settings_form($form);
}

/**
 * Delete comment thread
 */
function user_delete_comment_delete($cid = NULL) {
  $comment = db_fetch_object(db_query('SELECT c.*, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE c.cid = %d', $cid));
  _comment_delete_thread($comment);
  _comment_update_node_statistics($comment->nid);
}

Functions

Namesort descending Description
user_delete_backup Backup an user/node/comment object to the filesystem
user_delete_backup_remove_dir Recursive delete a folder with files
user_delete_backup_remove_expired Check if a folder is expired and delete
user_delete_backup_scan_expired Scan for and delete expired files
user_delete_comment_delete Delete comment thread
user_delete_cron Implementation of hook_cron().
user_delete_file_check_directory Copy of file_check_directory() without drupal_set_message(). see http://api.drupal.org/api/function/file_check_directory/5
user_delete_form_alter Implementation of hook_form_alter().
user_delete_menu Implementation of hook_menu().
user_delete_node_delete Copy of node_delete() whithout access check and drupal_set_message(). see http://api.drupal.org/api/function/node_delete/5
user_delete_perm Implementation of hook_perm().
user_delete_settings Administrative settings page
user_delete_submit Deal with the user/content after form submission

Constants

Namesort descending Description
USER_DELETE_FILE_PATH @file User delete - Let users delete their own account.