From db98c4438cd1231c62c19871fd30acfb39e730e6 Mon Sep 17 00:00:00 2001
From: Javier Casares
Date: Thu, 9 Jul 2026 06:28:55 +0000
Subject: [PATCH] v5.1.1
---
changelog.txt | 20 ++++++++
readme.txt | 24 +++++++++-
wpvulnerability-admin.php | 3 +-
wpvulnerability-adminms.php | 38 ---------------
wpvulnerability-debug.php | 36 +++++++-------
wpvulnerability-general.php | 4 --
wpvulnerability-notifications.php | 78 ++++++-------------------------
wpvulnerability-run.php | 1 -
wpvulnerability-schedule.php | 41 ++++++++++++++++
wpvulnerability-sitehealth.php | 3 ++
wpvulnerability-themes.php | 2 +-
wpvulnerability.php | 6 +--
12 files changed, 123 insertions(+), 133 deletions(-)
diff --git a/changelog.txt b/changelog.txt
index 854cfb8..975fa5c 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,25 @@
== Changelog ==
+= [5.1.1] - 2026-07-09 =
+
+**Fixed**
+
+* Site Health: the memcached, Redis, and SQLite vulnerability tests always returned "Invalid software type" because those components were missing from the software list. They now run correctly.
+* "Send test email" failed when no email recipients were configured: the forced-test path added the admin email but the send gate used a flag computed before the override. The test email now sends.
+* "Run notification now" reported failure when only webhook channels (Slack/Teams/Discord/Telegram) were enabled and email was disabled. The notification result now reflects whether any channel delivered successfully.
+* Weekly notification cron events were never auto-scheduled, because the `weekly` cron schedule was registered only at `init` while the on-load scheduling runs earlier. The weekly/daily schedule registration moved to the always-loaded scheduler so weekly notifications schedule correctly.
+* Deactivation deleted the per-component analysis settings (`wpvulnerability-analyze`), so deactivating and reactivating reset which components were hidden. Analysis settings are now preserved on deactivation (only uninstall removes them).
+* The debug "Cron Status" panel always showed the notification event as "not scheduled" because it checked the wrong hook name (`wpvulnerability_send_notification` instead of `wpvulnerability_notification`).
+* The debug "last run" timestamp was always empty because it read a `wpvulnerability-logs` option that is never written; it now reads the most recent API log entry.
+* Redis detection called `close()` twice on the success path (once in the `try` block, once in `finally`); the redundant close was removed.
+* The single-site vulnerable-themes count was hardcoded to `0` instead of reading the stored option.
+
+**Changed**
+
+* Single-site email recipient sanitization now uses `is_email()` for strict validation, matching the multisite behaviour.
+* Removed a redundant nonce field from the single-site "Reset Plugin" form (the form only submits the full-reset action).
+* Removed dead code: the unused `wpvulnerability_sanitize_messages` callback and its setting registration, and an unused `$tools_action` variable in the multisite admin.
+
= [5.1.0] - 2026-07-08 =
**Security**
diff --git a/readme.txt b/readme.txt
index cf3f792..8e27bd3 100644
--- a/readme.txt
+++ b/readme.txt
@@ -3,9 +3,9 @@ Contributors: javiercasares, davidperez, lbonomo, alexclassroom
Tags: security, vulnerability, site-health
Requires at least: 5.6
Tested up to: 7.1
-Stable tag: 5.1.0
+Stable tag: 5.1.1
Requires PHP: 7.0
-Version: 5.1.0
+Version: 5.1.1
License: GPL-3.0-or-later
License URI: https://spdx.org/licenses/GPL-3.0-or-later.html
@@ -201,6 +201,26 @@ First of all, peace of mind. Investigate what the vulnerability is and, above al
== Changelog ==
+= [5.1.1] - 2026-07-09 =
+
+**Fixed**
+
+* Site Health: the memcached, Redis, and SQLite vulnerability tests always returned "Invalid software type" because those components were missing from the software list. They now run correctly.
+* "Send test email" failed when no email recipients were configured: the forced-test path added the admin email but the send gate used a flag computed before the override. The test email now sends.
+* "Run notification now" reported failure when only webhook channels (Slack/Teams/Discord/Telegram) were enabled and email was disabled. The notification result now reflects whether any channel delivered successfully.
+* Weekly notification cron events were never auto-scheduled, because the `weekly` cron schedule was registered only at `init` while the on-load scheduling runs earlier. The weekly/daily schedule registration moved to the always-loaded scheduler so weekly notifications schedule correctly.
+* Deactivation deleted the per-component analysis settings (`wpvulnerability-analyze`), so deactivating and reactivating reset which components were hidden. Analysis settings are now preserved on deactivation (only uninstall removes them).
+* The debug "Cron Status" panel always showed the notification event as "not scheduled" because it checked the wrong hook name (`wpvulnerability_send_notification` instead of `wpvulnerability_notification`).
+* The debug "last run" timestamp was always empty because it read a `wpvulnerability-logs` option that is never written; it now reads the most recent API log entry.
+* Redis detection called `close()` twice on the success path (once in the `try` block, once in `finally`); the redundant close was removed.
+* The single-site vulnerable-themes count was hardcoded to `0` instead of reading the stored option.
+
+**Changed**
+
+* Single-site email recipient sanitization now uses `is_email()` for strict validation, matching the multisite behaviour.
+* Removed a redundant nonce field from the single-site "Reset Plugin" form (the form only submits the full-reset action).
+* Removed dead code: the unused `wpvulnerability_sanitize_messages` callback and its setting registration, and an unused `$tools_action` variable in the multisite admin.
+
= [5.1.0] - 2026-07-08 =
**Security**
diff --git a/wpvulnerability-admin.php b/wpvulnerability-admin.php
index 51e5852..1159d05 100644
--- a/wpvulnerability-admin.php
+++ b/wpvulnerability-admin.php
@@ -1437,7 +1437,6 @@ function wpvulnerability_render_admin_tab_tools() {
@@ -2211,7 +2210,7 @@ function wpvulnerability_admin_sanitize( $input ) {
foreach ( $input_email_text as $input_email ) {
$input_email = sanitize_email( trim( (string) $input_email ) );
- if ( $input_email ) {
+ if ( is_email( $input_email ) ) {
$input_emails[] = $input_email;
}
}
diff --git a/wpvulnerability-adminms.php b/wpvulnerability-adminms.php
index 1867698..0dc81f9 100644
--- a/wpvulnerability-adminms.php
+++ b/wpvulnerability-adminms.php
@@ -1447,14 +1447,6 @@ function wpvulnerability_render_network_admin_tab_logs() {
function wpvulnerability_render_network_admin_tab_tools() {
global $wpvulnerability_settings;
- $tools_action = add_query_arg(
- array(
- 'page' => 'wpvulnerability-options',
- 'tab' => 'tools',
- ),
- network_admin_url( 'settings.php' )
- );
-
$wpvulnerability_cron_config = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
if ( ! is_array( $wpvulnerability_cron_config ) ) {
$wpvulnerability_cron_config = array();
@@ -2959,24 +2951,6 @@ function wpvulnerability_sanitize_config( $input ) {
return $sanitized;
}
-/**
- * Sanitizes the messages generated by the plugin (simple messages).
- *
- * @since 2.0.0
- *
- * @param array $input Input values.
- * @return array Sanitized values.
- */
-function wpvulnerability_sanitize_messages( $input ) {
- $sanitized = array();
-
- foreach ( $input as $key => $message ) {
- $sanitized[ sanitize_key( (string) $key ) ] = sanitize_text_field( is_scalar( $message ) ? (string) $message : '' );
- }
-
- return $sanitized;
-}
-
/**
* Strictly sanitizes the analysis options (booleans).
*
@@ -3037,18 +3011,6 @@ function wpvulnerability_admin_init() {
)
);
- // Register messages.
- register_setting(
- 'admin_wpvulnerability_settings',
- 'wpvulnerability-messages',
- array(
- 'sanitize_callback' => 'wpvulnerability_sanitize_messages',
- 'default' => array(),
- 'show_in_rest' => false,
- 'type' => 'array',
- )
- );
-
// Add a section to the settings page.
add_settings_section(
'admin_wpvulnerability_settings',
diff --git a/wpvulnerability-debug.php b/wpvulnerability-debug.php
index b7a9da4..459ebe9 100644
--- a/wpvulnerability-debug.php
+++ b/wpvulnerability-debug.php
@@ -494,7 +494,7 @@ function wpvulnerability_debug_get_cron_status() {
'scheduled' => false,
),
'send_notification' => array(
- 'hook' => 'wpvulnerability_send_notification',
+ 'hook' => 'wpvulnerability_notification',
'next_run' => null,
'last_run' => null,
'scheduled' => false,
@@ -509,28 +509,28 @@ function wpvulnerability_debug_get_cron_status() {
}
// Check notification cron.
- $next_notification = wp_next_scheduled( 'wpvulnerability_send_notification' );
+ $next_notification = wp_next_scheduled( 'wpvulnerability_notification' );
if ( $next_notification ) {
$cron_status['send_notification']['next_run'] = $next_notification;
$cron_status['send_notification']['scheduled'] = true;
}
- // Try to get last run times from logs.
- $logs_raw = is_multisite() ? get_site_option( 'wpvulnerability-logs', array() ) : get_option( 'wpvulnerability-logs', array() );
- $logs = is_array( $logs_raw ) ? $logs_raw : array();
- if ( ! empty( $logs ) ) {
- // Get the most recent log entry for each type.
- foreach ( array_reverse( $logs ) as $log ) {
- if ( ! is_array( $log ) ) {
- continue; }
- if ( isset( $log['time'] ) && isset( $log['url'] ) ) {
- $timestamp = $log['time'];
-
- // Check if this is an update-related log.
- if ( ! $cron_status['update_database']['last_run'] ) {
- $cron_status['update_database']['last_run'] = $timestamp;
- }
- }
+ // Last run = most recent API response log (stored as the wpvulnerability_log CPT).
+ $last_log = get_posts(
+ array(
+ 'post_type' => 'wpvulnerability_log',
+ 'post_status' => 'any',
+ 'posts_per_page' => 1,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'fields' => 'ids',
+ 'no_found_rows' => true,
+ )
+ );
+ if ( ! empty( $last_log ) ) {
+ $timestamp = get_post_timestamp( $last_log[0] );
+ if ( false !== $timestamp ) {
+ $cron_status['update_database']['last_run'] = $timestamp;
}
}
diff --git a/wpvulnerability-general.php b/wpvulnerability-general.php
index 9a21e43..17884c5 100644
--- a/wpvulnerability-general.php
+++ b/wpvulnerability-general.php
@@ -1070,10 +1070,6 @@ function wpvulnerability_detect_redis() {
$result['method'] = 'redis_extension';
$result['reliability'] = 90;
- if ( $temporary_connection ) {
- $redis_client->close();
- }
-
return $result;
}
}
diff --git a/wpvulnerability-notifications.php b/wpvulnerability-notifications.php
index 1fda631..981e5fa 100644
--- a/wpvulnerability-notifications.php
+++ b/wpvulnerability-notifications.php
@@ -9,60 +9,6 @@
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
-/**
- * Adds a custom schedule for a weekly cron job.
- *
- * This function adds a new schedule interval of one week (604800 seconds)
- * to the system's available cron schedules. It allows tasks to be scheduled
- * to run every week using the 'weekly' interval.
- *
- * @since 2.0.0
- *
- * @param array> $schedules The existing system schedules.
- *
- * @return array> The updated list of schedules with the added weekly interval.
- */
-function wpvulnerability_add_every_week( $schedules ) {
- // Add a weekly schedule interval of 604800 seconds (1 week).
- // Use did_action('init') guard: cron_schedules fires before init (WP 6.7+ textdomain notice).
- $schedules['weekly'] = array(
- 'interval' => 604800,
- 'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
- );
-
- // Return the modified list of schedules.
- return $schedules;
-}
-// Hook the function to the 'cron_schedules' filter to add the custom schedule.
-add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
-
-/**
- * Adds a custom schedule for daily events.
- *
- * This function adds a new schedule interval of one day (86400 seconds)
- * to the system's available cron schedules. It allows tasks to be scheduled
- * to run every day using the 'daily' interval.
- *
- * @since 2.0.0
- *
- * @param array> $schedules List of available schedules.
- *
- * @return array> Modified list of available schedules with the added daily interval.
- */
-function wpvulnerability_add_every_day( $schedules ) {
- // Define a new schedule with a 24 hour interval.
- // Use did_action('init') guard: cron_schedules fires before init (WP 6.7+ textdomain notice).
- $schedules['daily'] = array(
- 'interval' => 86400,
- 'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
- );
-
- // Return the modified list of schedules.
- return $schedules;
-}
-// Hook the function to the 'cron_schedules' filter to add the custom schedule.
-add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
-
/**
* Disables email notifications when requested via URL.
*
@@ -715,7 +661,7 @@ function wpvulnerability_execute_notification( $forced = false ) {
$wpmail = false;
- if ( $email_enabled ) {
+ if ( $email_enabled || $forced ) {
$mail_to = is_array( $wpvulnerability_settings['emails'] ) ? array_map(
static function ( $e ) {
return is_scalar( $e ) ? (string) $e : '';
@@ -728,23 +674,27 @@ function wpvulnerability_execute_notification( $forced = false ) {
$text_message_body = wpvulnerability_html_to_plain_text( $email_content );
$text_message = trim( (string) ( $email_subject . "\n\n" . $text_message_body ) );
- if ( $slack_enabled ) {
- wpvulnerability_send_slack_notification( ( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ), $text_message );
+ // Track whether any webhook channel delivered successfully, so a webhook-only
+ // notification (email disabled) is not reported as a failure.
+ $webhook_sent = false;
+
+ if ( $slack_enabled && wpvulnerability_send_slack_notification( ( is_scalar( $wpvulnerability_settings['slack_webhook'] ) ? (string) $wpvulnerability_settings['slack_webhook'] : '' ), $text_message ) ) {
+ $webhook_sent = true;
}
- if ( $teams_enabled ) {
- wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message );
+ if ( $teams_enabled && wpvulnerability_send_teams_notification( ( is_scalar( $wpvulnerability_settings['teams_webhook'] ) ? (string) $wpvulnerability_settings['teams_webhook'] : '' ), $text_message ) ) {
+ $webhook_sent = true;
}
- if ( $discord_enabled ) {
- wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message );
+ if ( $discord_enabled && wpvulnerability_send_discord_notification( ( is_scalar( $wpvulnerability_settings['discord_webhook'] ) ? (string) $wpvulnerability_settings['discord_webhook'] : '' ), $text_message ) ) {
+ $webhook_sent = true;
}
- if ( $telegram_enabled ) {
- wpvulnerability_send_telegram_notification( ( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ), ( is_scalar( $wpvulnerability_settings['telegram_chat_id'] ) ? (string) $wpvulnerability_settings['telegram_chat_id'] : '' ), $text_message );
+ if ( $telegram_enabled && wpvulnerability_send_telegram_notification( ( is_scalar( $wpvulnerability_settings['telegram_bot_token'] ) ? (string) $wpvulnerability_settings['telegram_bot_token'] : '' ), ( is_scalar( $wpvulnerability_settings['telegram_chat_id'] ) ? (string) $wpvulnerability_settings['telegram_chat_id'] : '' ), $text_message ) ) {
+ $webhook_sent = true;
}
- return $wpmail;
+ return $wpmail || $webhook_sent;
}
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOM properties follow upstream naming.
diff --git a/wpvulnerability-run.php b/wpvulnerability-run.php
index 707d30d..147b2fa 100644
--- a/wpvulnerability-run.php
+++ b/wpvulnerability-run.php
@@ -365,7 +365,6 @@ function wpvulnerability_deactivation() {
$options = array(
'wpvulnerability_settings',
'wpvulnerability-data',
- 'wpvulnerability-analyze',
'wpvulnerability-themes',
'wpvulnerability-themes-cache',
'wpvulnerability-themes-vulnerable',
diff --git a/wpvulnerability-schedule.php b/wpvulnerability-schedule.php
index 5916b2d..c409f92 100644
--- a/wpvulnerability-schedule.php
+++ b/wpvulnerability-schedule.php
@@ -37,6 +37,47 @@ function wpvulnerability_add_every_six_hours( $schedules ) {
return $schedules;
}
+// Add weekly and daily schedules used by the notification cron.
+// These must be registered in this file (always loaded pre-init) so that the
+// on-load notification scheduling at the bottom of this file can resolve the
+// 'weekly' schedule before notifications.php is loaded at init.
+add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
+add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
+
+/**
+ * Registers a custom weekly cron schedule.
+ *
+ * @since 2.0.0
+ *
+ * @param array> $schedules Existing schedules.
+ * @return array> Schedules with the weekly interval added.
+ */
+function wpvulnerability_add_every_week( $schedules ) {
+ $schedules['weekly'] = array(
+ 'interval' => 604800,
+ 'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
+ );
+
+ return $schedules;
+}
+
+/**
+ * Registers a custom daily cron schedule.
+ *
+ * @since 2.0.0
+ *
+ * @param array> $schedules Existing schedules.
+ * @return array> Schedules with the daily interval added.
+ */
+function wpvulnerability_add_every_day( $schedules ) {
+ $schedules['daily'] = array(
+ 'interval' => 86400,
+ 'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
+ );
+
+ return $schedules;
+}
+
// Remove legacy scheduled events on subsites in multisite installs.
if ( is_multisite() && ! is_main_site() ) {
wpvulnerability_clear_plugin_cron_hooks();
diff --git a/wpvulnerability-sitehealth.php b/wpvulnerability-sitehealth.php
index 295b121..1834667 100644
--- a/wpvulnerability-sitehealth.php
+++ b/wpvulnerability-sitehealth.php
@@ -182,6 +182,9 @@ function wpvulnerability_test_software( $software ) {
'mysql' => __( 'MySQL', 'wpvulnerability' ),
'imagemagick' => __( 'ImageMagick', 'wpvulnerability' ),
'curl' => __( 'curl', 'wpvulnerability' ),
+ 'memcached' => __( 'memcached', 'wpvulnerability' ),
+ 'redis' => __( 'Redis', 'wpvulnerability' ),
+ 'sqlite' => __( 'SQLite', 'wpvulnerability' ),
);
if ( ! array_key_exists( $software, $software_list ) ) {
diff --git a/wpvulnerability-themes.php b/wpvulnerability-themes.php
index f3819d1..540ad83 100644
--- a/wpvulnerability-themes.php
+++ b/wpvulnerability-themes.php
@@ -687,7 +687,7 @@ function wpvulnerability_themes_view( $views ) {
return $views;
}
- $raw_count = is_multisite() ? get_site_option( 'wpvulnerability-themes-vulnerable', '0' ) : '0';
+ $raw_count = is_multisite() ? get_site_option( 'wpvulnerability-themes-vulnerable', '0' ) : get_option( 'wpvulnerability-themes-vulnerable', '0' );
$wpvulnerability_themes_total = ( is_scalar( json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) ) ? (int) json_decode( is_string( $raw_count ) ? $raw_count : '0', true ) : 0 );
if ( is_multisite() && is_network_admin() ) {
diff --git a/wpvulnerability.php b/wpvulnerability.php
index 7a50f3f..d4aabeb 100644
--- a/wpvulnerability.php
+++ b/wpvulnerability.php
@@ -5,7 +5,7 @@
* Description: Receive information about possible vulnerabilities in your WordPress from WordPress Vulnerability Database API.
* Requires at least: 5.6
* Requires PHP: 7.0
- * Version: 5.1.0
+ * Version: 5.1.1
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.es/
* License: GPL-3.0-or-later
@@ -16,7 +16,7 @@
*
* @package WPVulnerability
*
- * @version 5.1.0
+ * @version 5.1.1
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
@@ -24,7 +24,7 @@ defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/**
* Set some constants that I can change in future versions.
*/
-define( 'WPVULNERABILITY_PLUGIN_VERSION', '5.1.0' );
+define( 'WPVULNERABILITY_PLUGIN_VERSION', '5.1.1' );
define( 'WPVULNERABILITY_API_HOST', 'https://www.wpvulnerability.net/' );
/**