* * 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 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' => 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 Anonymous user.'), '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' => '
'. 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; } /** * 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' => '', '%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); }