* * 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 */ function user_delete_access($account) { global $user; return (user_access('administer users') || (user_access('delete own account') && $account->uid == $user->uid)); } /** * Implementation of hook_menu_alter(). */ function user_delete_menu_alter(&$callbacks) { $callbacks['user/%user/delete']['access callback'] = '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) { $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 Anonymous user. 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 Anonymous user.'), '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); 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' => '
'. 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: !htaccess", $variables)); watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $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(); }