Contents
Introducción
Registrar logs de actividad de usuario en WordPress es fundamental para auditoría, seguridad, diagnóstico de errores y cumplimiento normativo. Usando los hooks nativos de WordPress (actions y filters) podemos interceptar eventos relevantes —inicios de sesión, ediciones de contenidos, cambios de perfil, intentos fallidos de login, borrados— y persistir esa información en un repositorio controlado. Este tutorial explica, con ejemplos completos en PHP, cómo diseñar, implementar y consumir un sistema de logging de actividad basado en hooks.
Conceptos básicos: hooks en WordPress
Los hooks permiten ejecutar código en puntos concretos del ciclo de vida de WordPress. Hay dos tipos principales:
- Actions: Permiten ejecutar funciones cuando ocurre un evento (ej. wp_login, wp_logout, profile_update).
- Filters: Permiten modificar datos pasados entre funciones (se usan menos para logging, salvo casos concretos).
Para registrar actividades normalmente nos apoyamos en actions: nos suscribimos a una action y dentro de nuestra función recopilamos datos y los persistimos.
Decisiones de diseño
- Qué registrar: usuario (ID, login), acción (tipo), objeto afectado (post ID, tipo), timestamp, IP, user agent, metadatos opcionales (motivo, estado previo, cambios old->new).
- Dónde almacenar: tabla personalizada en la base de datos (recomendado), custom post type (posible pero menos óptimo para grandes volúmenes), logs externos (SIEM, Sentry, ELK), o ficheros.
- Retención y privacidad: evitar almacenar datos sensibles innecesarios, enmascarar PII, definir políticas de retención y purgado automático.
- Performance: considerar inserciones asíncronas (queues, Action Scheduler), índice en columnas frecuentes (user_id, action, created_at).
Esquema de la tabla de logs
Columna | Tipo | Descripción |
---|---|---|
id | BIGINT UNSIGNED AUTO_INCREMENT | PK |
user_id | BIGINT UNSIGNED | ID del usuario que realiza la acción (0 si anónimo) |
action | VARCHAR(100) | Nombre de la acción (ej. login, logout, publish_post) |
object_id | BIGINT UNSIGNED NULL | ID del recurso afectado (post_id, term_id…) |
object_type | VARCHAR(50) NULL | Tipo del recurso (post, user, comment) |
meta | TEXT NULL | JSON con información adicional |
ip_address | VARCHAR(45) NULL | IP del usuario |
user_agent | TEXT NULL | User agent del navegador |
created_at | DATETIME | Timestamp del evento |
Crear la tabla en la activación del plugin
Ejemplo mínimo de plugin que crea la tabla mediante dbDelta. Guardar como archivo PHP en wp-content/plugins/mi-activity-logger/mi-activity-logger.php
prefix . user_activity_logs charset_collate = wpdb->get_charset_collate() sql = CREATE TABLE {table_name} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, action VARCHAR(100) NOT NULL, object_id BIGINT UNSIGNED NULL, object_type VARCHAR(50) NULL, meta TEXT NULL, ip_address VARCHAR(45) NULL, user_agent TEXT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY user_id (user_id), KEY action (action), KEY created_at (created_at) ) {charset_collate} require_once(ABSPATH . wp-admin/includes/upgrade.php) dbDelta(sql) }
Helper para insertar logs
Centralizamos la inserción para facilitar cambios futuros (por ejemplo, enviar a un endpoint externo). Usamos wpdb->insert para evitar inyecciones.
prefix . user_activity_logs data = array( user_id => absint(user_id), action => sanitize_text_field(action), object_id => object_id ? absint(object_id) : null, object_type=> object_type ? sanitize_text_field(object_type) : null, meta => ! empty(meta) ? wp_json_encode(meta) : null, ip_address => mal_get_client_ip(), user_agent => isset(_SERVER[HTTP_USER_AGENT]) ? sanitize_text_field(_SERVER[HTTP_USER_AGENT]) : null, created_at => current_time(mysql), ) format = array(%d, %s, %d, %s, %s, %s, %s) // Ajustar formato según presencia de valores nulos wpdb->insert(table, data, format) }
Ejemplos de hooks comunes
1) Inicio de sesión
Usamos la action wp_login que recibe el nombre de usuario y el objeto WP_User puede obtenerse si es necesario.
ID : 0 mal_log_activity(login, user_id, null, user, array(login => user_login)) }
2) Logout
3) Intento de login fallido
username)) }4) Publicación o actualización de post
Para capturar cuando un post se crea o actualiza conviene usar transition_post_status o save_post. Aquí un ejemplo que diferencia publish y update.
ID, post->post_type, array(title => post->post_title)) } elseif (new_status === publish old_status === publish) { mal_log_activity(update_post, get_current_user_id(), post->ID, post->post_type, array(title => post->post_title)) } }5) Borrado de post
post_type, array(title => post->post_title)) } }6) Actualización de perfil
isset(old_user_data->user_email) ? old_user_data->user_email : , new_user_email => isset(new_user->user_email) ? new_user->user_email : , )) }Interfaz administrativa simple para ver logs
Un ejemplo básico que añade un menú y muestra las últimas entradas. En producción recomendamos usar clases y paginación.
prefix . user_activity_logs rows = wpdb->get_results(SELECT FROM {table} ORDER BY created_at DESC LIMIT 200) echo lth2gtÚltimas entradas de Activity Logslt/h2gt echo lttable class=widefat fixedgt echo lttheadgtlttrgtltthgtIDlt/thgtltthgtUsuariolt/thgtltthgtAcciónlt/thgtltthgtObjetolt/thgtltthgtMetalt/thgtltthgtIPlt/thgtltthgtFechalt/thgtlt/trgtlt/theadgt echo lttbodygt foreach (rows as r) { user_display = r->user_id ? esc_html(get_userdata(r->user_id) ? get_userdata(r->user_id)->user_login : r->user_id) : anon meta = r->meta ? esc_html(r->meta) : echo lttrgt echo lttdgt . esc_html(r->id) . lt/tdgt echo lttdgt . user_display . lt/tdgt echo lttdgt . esc_html(r->action) . lt/tdgt echo lttdgt . esc_html(r->object_type . . r->object_id) . lt/tdgt echo lttdgt . meta . lt/tdgt echo lttdgt . esc_html(r->ip_address) . lt/tdgt echo lttdgt . esc_html(r->created_at) . lt/tdgt echo lt/trgt } echo lt/tbodygt echo lt/tablegt }Enviar logs a un servicio externo (opcional)
Si se prefiere centralizar en un sistema externo, se puede enviar cada evento con wp_remote_post o acumular y enviar por lotes.
wp_json_encode(payload), headers => array(Content-Type => application/json), timeout => 5, ) resp = wp_remote_post(endpoint, args) // manejar errores si es necesario } // Dentro de mal_log_activity, se podría añadir: payload = array_merge(data, array(site => get_bloginfo(url))) mal_send_log_to_external(payload)Consideraciones de seguridad y privacidad
- No loguear contraseñas ni tokens.
- Enmascarar o truncar datos personales cuando sea posible.
- Restringir acceso al admin de logs mediante capabilities (ej. manage_options).
- Encriptar datos sensibles si deben almacenarse (revisar legislación como GDPR).
- Validar y escapar todo lo mostrado en el panel (esc_html, esc_attr).
Performance y escalabilidad
- Si el sitio genera muchos eventos, evitar hacer wp_remote_post o consultas pesadas de forma síncrona.
- Valorar Action Scheduler o un sistema de colas para procesar logs en background.
- Crear índices en columnas consultadas frecuentemente (user_id, action, created_at).
- Archivar o purgar logs antiguos automáticamente con un cron o WP-CLI.
Pruebas y depuración
- Probar cada hook manualmente: iniciar/cerrar sesión, crear/editar/borrar posts, actualizar perfil.
- Revisar la tabla en la base de datos para comprobar integridad y tipos.
- Comprobar headers HTTP y proxies para la IP real si usas X-Forwarded-For.
- Habilitar logs temporales (error_log) para detectar fallos en el insert.
Buenas prácticas finales
- Centralizar la lógica de logging en funciones reutilizables.
- Documentar las acciones registradas y el significado de cada action y meta.
- Implementar rotación/purgado de logs y mecanismos de backup.
- Probar impacto en rendimiento en staging antes de desplegar en producción.
Recursos
Conclusión
Usando hooks en WordPress y una tabla personalizada se puede implementar un sistema de logging de actividad robusto y flexible. El ejemplo mostrado cubre creación de tabla, función central de inserción, hooks para eventos habituales y una interfaz administrativa básica. Antes de producir, ajusta retención, privacidad y rendimiento según las necesidades del proyecto.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |