*
* 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);
}