<?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
 * 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() {
  $items['admin/user/user_delete'] = array(
    'title' => 'User delete',
    'description' => "Configure the user delete action.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array('user_delete_settings'),
    'access arguments' => array('administer users'),
    'file' => 'user_delete.admin.inc',
  );
  return $items;
}

/**
 * Checks whether a user can delete an account.
 *
 * Unique function name required to avoid clash with core user_delete_access().
 */
function user_delete_user_delete_access($account) {
  global $user;
  return ((user_access('administer users') || (user_access('delete own account') && $account->uid == $user->uid)) && $account->uid > 0);
}

/**
 * Implementation of hook_menu_alter().
 */
function user_delete_menu_alter(&$callbacks) {
  $callbacks['user/%user/delete']['access callback'] = 'user_delete_user_delete_access';
  $callbacks['user/%user/delete']['access arguments'] = array(1);
  $callbacks['user/%user/delete']['type'] = MENU_CALLBACK;
}

/**
 * Implementation of hook_form_alter().
 */
function user_delete_form_alter(&$form, $form_state, $form_id) {
  global $user;

  if ($form_id == 'user_profile_form') {
    //access check
    if (user_access('delete own account') && $form['#uid'] == $user->uid && arg(3)=='') {
      $form['delete'] = array(
        '#type' => 'submit',
        '#value' => t('Delete Account'),
        '#weight' => 31,
        '#submit' => array('user_edit_delete_submit'),
      );
    }
  }

  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' => -10,
      );
    }
    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['_account']['#value']->uid;
    $form['#submit'] = array('user_delete_submit');
  }
}

/**
 * Deal with the user/content after form submission
 */
function user_delete_submit($form, &$form_state) {
  global $user;
  $default_op = variable_get('user_delete_default_action', 0);
  $op = ($default_op) ? $default_op : $form_state['values']['user_delete_action'];
  $uid = $form_state['values']['_account']->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);
      $edit['status'] = 0;
      module_invoke_all('user', 'update', $edit, $account);
      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';
  }
  elseif (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 (user_delete_scandir($dir) as $item) {
    if ($item == '.' || $item == '..') {
      continue;
    }
    if (!user_delete_backup_remove_dir($dir . DIRECTORY_SEPARATOR . $item)) {
      return FALSE;
    }
  }
  return rmdir($dir);
}

/**
 * Compat function for scandir() with php4 or php5
 */
function user_delete_scandir($dir) {
  if (function_exists('scandir')) {
    return scandir($dir);
  }
  else {
    $dh  = opendir($dir);
    while (false !== ($filename = readdir($dh))) {
      $files[] = $filename;
    }
    return $files;
  }
}

/**
 * Copy of node_delete() whithout access check and drupal_set_message().
 * see http://api.drupal.org/api/function/node_delete/6
 */
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/6
 */
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;
}

/**
 * Delete comment thread
 */
function user_delete_comment_delete($cid = NULL) {
  include_once(drupal_get_path('module', 'comment') . '/comment.admin.inc');

  $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);
  cache_clear_all();
}
