Skip to content

Instantly share code, notes, and snippets.

@TuWebMaster
Last active October 17, 2025 17:19
Show Gist options
  • Select an option

  • Save TuWebMaster/2872229a486e73032a9d2487d4fde2e9 to your computer and use it in GitHub Desktop.

Select an option

Save TuWebMaster/2872229a486e73032a9d2487d4fde2e9 to your computer and use it in GitHub Desktop.
Auto Bot Cleaner es un snippet PHP para WordPress que permite identificar y eliminar automáticamente usuarios sospechosos (bots) basándose en la longitud de los campos de registro y roles asignados. Esta herramienta limpia registros automáticos no deseados sin afectar a los usuarios reales.
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Auto Bot Cleaner
* Herramienta para limpiar usuarios automáticamente por longitud de campos
*/
add_action('admin_menu', function() {
add_users_page(
'Auto Bot Cleaner',
'Auto Bot Cleaner',
'manage_options',
'auto-bot-cleaner',
'twm_limpiar_bots_len_page'
);
});
function twm_build_role_join_where($roles_csv, &$params) {
global $wpdb;
$roles = array_filter(array_map('trim', explode(',', (string)$roles_csv)));
$join = "";
$where = "";
if (!empty($roles)) {
$join = " INNER JOIN {$wpdb->usermeta} umcap ON umcap.user_id = u.ID AND umcap.meta_key = %s ";
$params[] = $wpdb->prefix . 'capabilities';
$likes = [];
foreach ($roles as $r) {
$likes[] = " umcap.meta_value LIKE %s ";
$params[] = '%' . $r . '%';
}
$where = " AND (" . implode(' OR ', $likes) . ") ";
}
return [$join, $where];
}
function twm_len_condition_sql(&$params, $min_chars) {
$sources = [
"CHAR_LENGTH(u.user_login) >= %d",
"CHAR_LENGTH(u.display_name) >= %d",
"CHAR_LENGTH(u.user_nicename) >= %d",
"CHAR_LENGTH(COALESCE(um_fn.meta_value,'')) >= %d",
"CHAR_LENGTH(COALESCE(um_ln.meta_value,'')) >= %d",
"CHAR_LENGTH(COALESCE(um_nn.meta_value,'')) >= %d",
];
foreach ($sources as $s) { $params[] = $min_chars; }
return ' AND (' . implode(' OR ', $sources) . ') ';
}
function twm_limpiar_bots_len_page() {
global $wpdb;
// Parámetros UI
$min_chars = isset($_POST['min_chars']) ? max(1, (int)$_POST['min_chars']) : 30;
$batch_size = isset($_POST['batch_size']) ? max(1, (int)$_POST['batch_size']) : 200;
$roles_csv = isset($_POST['roles_csv']) ? sanitize_text_field($_POST['roles_csv']) : 'subscriber,customer';
$do_prescan = isset($_POST['do_prescan']);
$do_auto = isset($_POST['do_auto']);
$auto_total = (int) get_transient('twm_lb_auto_deleted_total') ?: 0;
// === NUEVO: detectar "contexto de auto-borrado" ===
// Solo consideramos contexto si venimos de "Auto-borrar" o de un reenvío interno entre tandas.
$in_auto_context = ( isset($_POST['do_auto']) || isset($_POST['twm_auto_loop']) );
// Joins de metadatos de nombre
$join_meta = "
LEFT JOIN {$wpdb->usermeta} um_fn ON um_fn.user_id = u.ID AND um_fn.meta_key = 'first_name'
LEFT JOIN {$wpdb->usermeta} um_ln ON um_ln.user_id = u.ID AND um_ln.meta_key = 'last_name'
LEFT JOIN {$wpdb->usermeta} um_nn ON um_nn.user_id = u.ID AND um_nn.meta_key = 'nickname'
";
echo '<div class="wrap"><h1>Auto Bot Cleaner</h1>';
echo '<style>.twm-auto-announce{color:#b32d2e;font-weight:700;}</style>';
// === PRE-SCAN: conteo con criterios actuales ===
$params_c = [];
[$join_roles_c, $where_roles_c] = twm_build_role_join_where($roles_csv, $params_c);
$len_sql_c = twm_len_condition_sql($params_c, $min_chars);
$sql_count = "
SELECT COUNT(*)
FROM {$wpdb->users} u
{$join_roles_c}
{$join_meta}
WHERE 1=1
{$where_roles_c}
{$len_sql_c}
";
$remaining_before = (int)$wpdb->get_var($wpdb->prepare($sql_count, ...$params_c));
// Mostrar mensaje de carga inicial si es la primera tanda
if ($do_auto && !isset($_POST['twm_auto_loop'])) {
echo '<div class="notice notice-info">';
echo '<p>🔄 <strong>Preparando la limpieza automática...</strong></p>';
echo '<p>El proceso comenzará en breve. Por favor espera mientras preparamos todo.</p>';
echo '<p>Podrás ver el progreso en unos momentos.</p>';
echo '</div>';
// Configurar el formulario para la primera ejecución
?>
<form id="twm-auto-form" method="post">
<?php wp_nonce_field('twm_lb_len'); ?>
<input type="hidden" name="min_chars" value="<?php echo esc_attr($min_chars); ?>">
<input type="hidden" name="roles_csv" value="<?php echo esc_attr($roles_csv); ?>">
<input type="hidden" name="batch_size" value="<?php echo esc_attr($batch_size); ?>">
<input type="hidden" name="do_auto" value="1">
<input type="hidden" name="twm_auto_loop" value="1">
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Desplazarse al área de mensajes
const notice = document.querySelector('.notice');
if (notice) {
notice.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Enviar formulario después de un breve retraso
setTimeout(function() {
document.getElementById('twm-auto-form').submit();
}, 1000);
});
</script>
<?php
echo '</div>'; // .wrap
return;
}
if ( $do_auto && check_admin_referer('twm_lb_len') ) {
// === AUTO: borrar una tanda con límite batch_size ===
$params = [];
[$join_roles, $where_roles] = twm_build_role_join_where($roles_csv, $params);
$len_sql = twm_len_condition_sql($params, $min_chars);
$sql = "
SELECT u.ID
FROM {$wpdb->users} u
{$join_roles}
{$join_meta}
WHERE 1=1
{$where_roles}
{$len_sql}
ORDER BY
GREATEST(
CHAR_LENGTH(u.user_login),
CHAR_LENGTH(u.display_name),
CHAR_LENGTH(u.user_nicename),
CHAR_LENGTH(COALESCE(um_fn.meta_value,'')),
CHAR_LENGTH(COALESCE(um_ln.meta_value,'')),
CHAR_LENGTH(COALESCE(um_nn.meta_value,''))
) DESC
LIMIT %d
";
$params[] = $batch_size;
$rows = $wpdb->get_results($wpdb->prepare($sql, ...$params));
$deleted_this_batch = 0;
if (!empty($rows)) {
$current_user_id = get_current_user_id();
foreach ($rows as $u) {
if ((int)$u->ID === $current_user_id) continue; // no borrarte a vos
if (wp_delete_user((int)$u->ID)) {
$deleted_this_batch++;
}
}
// acumular progreso
$auto_total += $deleted_this_batch;
set_transient('twm_lb_auto_deleted_total', $auto_total, 60*10); // 10 min
// mantener bandera de "en curso" activa durante el loop
set_transient('twm_lb_auto_running', 1, 60*10);
}
}
// Recalcular restantes tras el borrado (o tras el pre-scan)
$params_c2 = [];
[$join_roles_c2, $where_roles_c2] = twm_build_role_join_where($roles_csv, $params_c2);
$len_sql_c2 = twm_len_condition_sql($params_c2, $min_chars);
$sql_count2 = "
SELECT COUNT(*)
FROM {$wpdb->users} u
{$join_roles_c2}
{$join_meta}
WHERE 1=1
{$where_roles_c2}
{$len_sql_c2}
";
$remaining_after = (int)$wpdb->get_var($wpdb->prepare($sql_count2, ...$params_c2));
// === Formulario principal ===
?>
<form method="post" style="margin-bottom:16px;">
<?php wp_nonce_field('twm_lb_len'); ?>
<table class="form-table">
<tr>
<th><label for="min_chars">Mínimo de caracteres</label></th>
<td><input type="number" id="min_chars" name="min_chars" value="<?php echo esc_attr($min_chars); ?>" min="1" step="1"></td>
</tr>
<tr>
<th><label for="roles_csv">Roles a revisar</label></th>
<td>
<input type="text" id="roles_csv" name="roles_csv" value="<?php echo esc_attr($roles_csv); ?>" style="width:320px">
<p class="description">Ej: <code>subscriber,customer</code>. Vacío = todos los roles.</p>
</td>
</tr>
<tr>
<th><label for="batch_size">Usuarios por tanda</label></th>
<td><input type="number" id="batch_size" name="batch_size" value="<?php echo esc_attr($batch_size); ?>" min="1" step="1"></td>
</tr>
</table>
<p>
<button class="button button-secondary" name="do_prescan" value="1">Pre-Scan</button>
<button class="button" name="do_auto" value="1">Auto-borrar todo por tandas</button>
</p>
</form>
<?php
// Feedback de Pre-Scan (si se apretó ese botón)
if ( $do_prescan ) {
echo '<div class="notice notice-info"><p>Pre-Scan aplicado con <strong>mínimo '
. esc_html($min_chars) . ' caracteres</strong> y roles: '
. esc_html($roles_csv ?: 'todos') . '.</p></div>';
}
echo '<h2>Resultados</h2>';
echo '<p>Usuarios que cumplen el criterio: <strong>' . number_format_i18n($remaining_after) . '</strong></p>';
// Mostrar progreso después de la primera tanda
if ($do_auto && isset($_POST['twm_auto_loop'])) {
if ($remaining_after > 0) {
// Calcular porcentaje completado
$total_processed = $auto_total + $remaining_after;
$progress_percent = $total_processed > 0 ?
min(100, round(($auto_total / $total_processed) * 100)) : 0;
echo '<div class="notice notice-info">';
echo '<h3>🔄 Procesando limpieza automática</h3>';
echo '<p>Por favor espera mientras se completan las operaciones.</p>';
echo '<div style="background: #f0f0f1; height: 20px; border-radius: 10px; margin: 10px 0; overflow: hidden;">';
echo '<div style="background: #2271b1; height: 100%; width: ' . esc_attr($progress_percent) . '%; transition: width 0.3s ease-in-out;"></div>';
echo '</div>';
echo '<p>Progreso: <strong>' . number_format_i18n($progress_percent) . '%</strong> (' . number_format_i18n($auto_total) . ' de ~' . number_format_i18n($total_processed) . ' usuarios procesados)</p>';
echo '<p>Usuarios eliminados en esta sesión: <strong>' . number_format_i18n($auto_total) . '</strong></p>';
echo '<p>Usuarios restantes: <strong>' . number_format_i18n($remaining_after) . '</strong></p>';
echo '</div>';
// Formulario para la siguiente tanda
?>
<form id="twm-auto-form" method="post">
<?php wp_nonce_field('twm_lb_len'); ?>
<input type="hidden" name="min_chars" value="<?php echo esc_attr($min_chars); ?>">
<input type="hidden" name="roles_csv" value="<?php echo esc_attr($roles_csv); ?>">
<input type="hidden" name="batch_size" value="<?php echo esc_attr($batch_size); ?>">
<input type="hidden" name="do_auto" value="1">
<input type="hidden" name="twm_auto_loop" value="1">
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Desplazarse al área de progreso
const notice = document.querySelector('.notice');
if (notice) {
notice.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Enviar formulario después de 1.2 segundos
setTimeout(function() {
document.getElementById('twm-auto-form').submit();
}, 1200);
});
</script>
<?php
} else {
// Proceso completado
delete_transient('twm_lb_auto_deleted_total');
echo '<div class="notice notice-success">';
echo '<h3>✅ Proceso de limpieza completado</h3>';
echo '<p>Se han eliminado <strong>' . number_format_i18n($auto_total) . '</strong> usuarios que coincidían con los criterios de búsqueda.</p>';
echo '</div>';
}
}
// Si hay transient pero NO estamos en contexto (refresh manual o visita nueva),
// no mostramos el banner para evitar confusión.
// Opcional: podrías limpiar el transient si no hay pendientes.
if ( ! $in_auto_context && get_transient('twm_lb_auto_running') && $remaining_after === 0 ) {
delete_transient('twm_lb_auto_deleted_total');
delete_transient('twm_lb_auto_running');
}
if ($do_auto) {
if ($remaining_after > 0) {
// Relanzar próxima tanda con feedback visual
?>
<form id="twm-auto-form" method="post">
<?php wp_nonce_field('twm_lb_len'); ?>
<input type="hidden" name="min_chars" value="<?php echo esc_attr($min_chars); ?>">
<input type="hidden" name="roles_csv" value="<?php echo esc_attr($roles_csv); ?>">
<input type="hidden" name="batch_size" value="<?php echo esc_attr($batch_size); ?>">
<input type="hidden" name="do_auto" value="1">
<input type="hidden" name="twm_auto_loop" value="1">
</form>
<script>
// Mostrar mensaje de progreso antes de enviar el formulario
document.addEventListener('DOMContentLoaded', function() {
const progressText = document.getElementById('progress-text');
const progressBar = document.getElementById('progress-bar');
// Actualizar UI antes de la siguiente tanda
progressText.innerHTML = 'Preparando siguiente tanda... <span style="color:#666;font-style:italic;">Por favor espere...</span>';
// Pequeña animación de carga
let dots = 0;
const dotInterval = setInterval(function() {
dots = (dots + 1) % 4;
progressText.innerHTML = 'Preparando siguiente tanda' + '.'.repeat(dots) + ' <span style="color:#666;font-style:italic;">Por favor espere...</span>';
}, 300);
// Enviar formulario después de un breve retraso
setTimeout(function() {
clearInterval(dotInterval);
document.getElementById('twm-auto-form').submit();
}, 1000);
});
</script>
<?php
} else {
// Fin del proceso
$done_total = (int) $auto_total;
delete_transient('twm_lb_auto_deleted_total');
delete_transient('twm_lb_auto_running');
echo '<div class="notice notice-success" style="padding: 15px 20px; margin: 20px 0; border-left: 4px solid #00a32a;">';
echo '<h3 style="margin: 0.5em 0; color: #00a32a;">✅ Proceso de limpieza completado con éxito</h3>';
echo '<p style="margin: 0.5em 0; font-size: 14px;">';
echo 'Se han eliminado un total de <strong>' . number_format_i18n($done_total) . '</strong> usuarios que coincidían con los criterios de búsqueda.';
echo '</p>';
echo '<p style="margin: 0.5em 0 0; font-size: 13px; color: #646970;">';
echo '<em>Puedes verificar los resultados o realizar una nueva búsqueda con diferentes parámetros si es necesario.</em>';
echo '</p>';
echo '</div>';
// Añadir script para scroll suave al mensaje de finalización
echo '<script>
document.addEventListener("DOMContentLoaded", function() {
const notice = document.querySelector(".notice-success");
if (notice) {
notice.scrollIntoView({ behavior: "smooth", block: "center" });
}
});
</script>';
}
}
// === Texto explicativo y créditos ===
echo '<div style="margin-top:20px; padding:12px; border:1px solid #ccc; background:#f9f9f9;">';
echo '<h2>Acerca de Auto Bot Cleaner</h2>';
echo '<p>Esta herramienta nació de la necesidad de borrar bots registrados en WordPress que compartían como patrón la presencia de publicidad o contenido sospechoso en alguno de los campos del registro. Para diferenciarlos de los usuarios reales, se utiliza un contador de caracteres en campos clave, permitiendo eliminar únicamente los registros sospechosos sin afectar cuentas legítimas.</p>';
echo '<p>Si necesitas adaptar esta herramienta a tus necesidades específicas, comunicate con <a href="https://www.tuwebmaster.com.ar" target="_blank" rel="noopener">www.tuwebmaster.com.ar</a>.</p>';
echo '<p style="font-size:12px; color:#666;">&copy; ' . date('Y') . ' TuWebmaster. Todos los derechos reservados.</p>';
echo '</div>';
echo '</div>'; // .wrap
}
@TuWebMaster

Copy link
Copy Markdown
Author

Hace unos días me tocó enfrentar un problema bastante puntual en uno de los sitios WordPress que administro: una base de datos inflada con más de 200.000 usuarios registrados que, en realidad, eran bots. No era la primera vez que veía registros automáticos, pero esta vez el volumen era tan grande que afectaba el rendimiento del sitio, las métricas de usuarios y hasta la seguridad general.

No soy muy fan de hacer publicaciones tipo “usa esta herramienta porque te va a cambiar la vida”, pero en este caso desarrollé un snippet que me permitió resolver el problema de forma bastante directa. Lo comparto por si a alguien más le sirve, ya sea para copiarlo tal cual o para adaptarlo a sus propios criterios.

Leer más:
https://tuwebmaster.com.ar/como-limpiar-usuarios-bots-en-wordpress/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment