wpvulnerability/wpvulnerability-debug.php
2026-08-23 07:08:42 +00:00

1083 lines
35 KiB
PHP

<?php
/**
* Debug functions
*
* @package WPVulnerability
*
* @version 4.3.0
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
// Load required plugin files if not already loaded.
if ( ! function_exists( 'wpvulnerability_analyze_filter' ) ) {
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-general.php';
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-run.php';
}
if ( ! function_exists( 'wpvulnerability_get_software_version' ) ) {
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-software.php';
}
/**
* Retrieves debug log file information.
*
* Detects the location of the WordPress debug log file and checks if it's accessible.
*
* @since 4.3.0
*
* @return array<string, mixed> Array with 'path', 'exists', 'size', 'url', and 'accessible' keys.
*/
function wpvulnerability_debug_get_log_file_info() {
$log_info = array(
'path' => null,
'exists' => false,
'size' => 0,
'url' => null,
'accessible' => false,
);
// Check if WP_DEBUG_LOG is enabled.
if ( ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) {
return $log_info;
}
// Determine log file path.
// WP_DEBUG_LOG can be a string (custom path) or true/false.
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_constant -- need runtime value for type narrowing
$wp_debug_log_value = defined( 'WP_DEBUG_LOG' ) ? WP_DEBUG_LOG : false;
if ( is_string( $wp_debug_log_value ) ) { // @phpstan-ignore function.impossibleType (WP_DEBUG_LOG may be a string path at runtime)
// Custom path specified.
$log_file = $wp_debug_log_value;
} else {
// Default path: wp-content/debug.log.
$log_file = WP_CONTENT_DIR . '/debug.log';
}
$log_info['path'] = $log_file;
// Check if file exists.
if ( file_exists( $log_file ) ) {
$log_info['exists'] = true;
// Get file size.
$size = filesize( $log_file );
if ( false !== $size ) {
$log_info['size'] = $size;
}
// Check if file is within wp-content (accessible via web).
$wp_content_dir = realpath( WP_CONTENT_DIR );
$log_file_real = realpath( $log_file );
if ( $wp_content_dir && $log_file_real && 0 === strpos( $log_file_real, $wp_content_dir ) ) {
// File is within wp-content, generate URL.
$relative_path = str_replace( $wp_content_dir, '', $log_file_real );
$relative_path = str_replace( '\\', '/', $relative_path );
$log_info['url'] = content_url() . $relative_path;
$log_info['accessible'] = true;
}
}
return $log_info;
}
/**
* Enhanced web server detection for debug purposes.
*
* Detects a wider range of web servers including nginx, Apache, LiteSpeed,
* Caddy, IIS, Angie, OpenLiteSpeed, and others.
*
* @since 4.3.0
*
* @return array<string, string> Array with 'name' and 'version' keys.
*/
function wpvulnerability_debug_detect_webserver() {
$webserver = array(
'name' => 'Unknown',
'version' => '',
);
// First try the plugin's standard detection.
$detected = wpvulnerability_detect_webserver();
if ( ! empty( $detected['name'] ) ) {
$webserver['name'] = $detected['name'];
if ( ! empty( $detected['version'] ) ) {
$webserver['version'] = $detected['version'];
}
}
// If still unknown, try enhanced detection from SERVER_SOFTWARE.
if ( 'Unknown' === $webserver['name'] && isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
$server_software = sanitize_text_field( wp_unslash( is_string( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '' ) );
$server_lower = strtolower( $server_software );
// LiteSpeed detection.
if ( false !== stripos( $server_lower, 'litespeed' ) ) {
if ( preg_match( '/litespeed/i', $server_software, $match ) ) {
$webserver['name'] = 'LiteSpeed';
if ( preg_match( '/litespeed\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
}
// OpenLiteSpeed detection.
if ( false !== stripos( $server_lower, 'openlitespeed' ) ) {
$webserver['name'] = 'OpenLiteSpeed';
if ( preg_match( '/openlitespeed\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
// Caddy detection.
if ( false !== stripos( $server_lower, 'caddy' ) ) {
$webserver['name'] = 'Caddy';
if ( preg_match( '/caddy\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
// IIS detection.
if ( false !== stripos( $server_lower, 'microsoft-iis' ) || false !== stripos( $server_lower, 'iis' ) ) {
$webserver['name'] = 'Microsoft IIS';
if ( preg_match( '/(?:microsoft-)?iis\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
// Angie detection (nginx fork).
if ( false !== stripos( $server_lower, 'angie' ) ) {
$webserver['name'] = 'Angie';
if ( preg_match( '/angie\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
// OpenResty detection (nginx-based).
if ( false !== stripos( $server_lower, 'openresty' ) ) {
$webserver['name'] = 'OpenResty';
if ( preg_match( '/openresty\/?(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
// Tengine detection (nginx fork).
if ( false !== stripos( $server_lower, 'tengine' ) ) {
$webserver['name'] = 'Tengine';
if ( preg_match( '/tengine\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
$webserver['version'] = $version_match[1];
}
}
}
// Try shell commands for additional detection if shell_exec is allowed.
if ( 'Unknown' === $webserver['name'] && function_exists( 'wpvulnerability_safe_shell_exec' ) ) {
// Try LiteSpeed. `which` prints the binary path on success; on failure it
// prints a "which: no litespeed ..." message to stderr. The wrapper merges
// stderr into stdout (2>&1), so validate that the output is actually a path.
$litespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which litespeed' );
if ( ! empty( $litespeed_test ) && 0 === strpos( trim( (string) $litespeed_test ), '/' ) ) {
$webserver['name'] = 'LiteSpeed';
}
// Try OpenLiteSpeed.
$openlitespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which openlitespeed' );
if ( ! empty( $openlitespeed_test ) && 0 === strpos( trim( (string) $openlitespeed_test ), '/' ) ) {
$webserver['name'] = 'OpenLiteSpeed';
}
// Try Caddy. The version regex guards against false positives: a "command
// not found" message does not match a version pattern.
$caddy_version = wpvulnerability_safe_shell_exec( 'apache', 'caddy version' );
if ( ! empty( $caddy_version ) && preg_match( '/v?(\d+\.\d+\.\d+)/', $caddy_version, $version_match ) ) {
$webserver['name'] = 'Caddy';
$webserver['version'] = $version_match[1];
}
}
return $webserver;
}
/**
* Retrieves comprehensive system information for debugging.
*
* @since 4.3.0
*
* @return array<string, mixed> Associative array containing system information.
*/
function wpvulnerability_debug_get_system_info() {
global $wp_version, $wpdb;
// Detect database type (MariaDB vs MySQL).
$sqlserver = wpvulnerability_detect_sqlserver();
$db_type = ! empty( $sqlserver['name'] ) ? $sqlserver['name'] : 'MySQL';
$db_version = ! empty( $sqlserver['version'] ) ? $sqlserver['version'] : $wpdb->db_version();
// Detect web server with enhanced detection.
$webserver = wpvulnerability_debug_detect_webserver();
$webserver_name = $webserver['name'];
if ( ! empty( $webserver['version'] ) ) {
$webserver_name .= ' ' . $webserver['version'];
}
$info = array(
'wordpress' => array(
'version' => $wp_version,
'multisite' => is_multisite(),
'language' => get_locale(),
'home_url' => home_url(),
'site_url' => site_url(),
),
'php' => array(
'version' => phpversion(),
'extensions' => array(
'curl' => extension_loaded( 'curl' ),
'json' => extension_loaded( 'json' ),
'mbstring' => extension_loaded( 'mbstring' ),
'xml' => extension_loaded( 'xml' ),
'zip' => extension_loaded( 'zip' ),
),
'memory' => array(
'limit' => ini_get( 'memory_limit' ),
'usage' => size_format( memory_get_usage( true ) ),
'peak' => size_format( memory_get_peak_usage( true ) ),
),
),
'database' => array(
'type' => $db_type,
'version' => $db_version,
),
'webserver' => array(
'software' => $webserver_name,
),
'debug' => array(
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
'wp_debug_display' => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
'script_debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
'log_file' => wpvulnerability_debug_get_log_file_info(),
),
'plugin' => array(
'version' => WPVULNERABILITY_PLUGIN_VERSION,
'path' => WPVULNERABILITY_PLUGIN_PATH,
),
);
return $info;
}
/**
* Retrieves the status of all trackable components.
*
* @since 4.3.0
*
* @return array<int, array<string, mixed>> Array of component statuses.
*/
function wpvulnerability_debug_get_component_status() {
$components = array(
'core',
'plugins',
'themes',
'php',
'apache',
'nginx',
'mysql',
'mariadb',
'imagemagick',
'curl',
'memcached',
'redis',
'sqlite',
);
$status = array();
foreach ( $components as $component ) {
$version = null;
$detected = false;
$analyzed = wpvulnerability_analyze_filter( $component );
$cache_time = is_multisite()
? get_site_option( 'wpvulnerability-' . $component . '-cache' )
: get_option( 'wpvulnerability-' . $component . '-cache' );
// Decode cache time if it's JSON-encoded.
if ( $cache_time && is_string( $cache_time ) ) {
$decoded = json_decode( $cache_time );
if ( null !== $decoded ) {
$cache_time = $decoded;
}
}
// Determine detection status and version.
switch ( $component ) {
case 'core':
global $wp_version;
$version = $wp_version;
$detected = true;
break;
case 'plugins':
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$version = count( $all_plugins ) . ' installed';
$detected = true;
break;
case 'themes':
$all_themes = wp_get_themes();
$version = count( $all_themes ) . ' installed';
$detected = true;
break;
case 'php':
case 'apache':
case 'nginx':
case 'mysql':
case 'mariadb':
case 'imagemagick':
case 'curl':
case 'memcached':
case 'redis':
case 'sqlite':
if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
$version = wpvulnerability_get_software_version( $component );
if ( null !== $version ) {
$detected = true;
}
}
break;
}
// Calculate cache status.
$cache_status = 'No cache';
if ( $cache_time && is_numeric( $cache_time ) ) {
$cache_time_int = (int) $cache_time;
$time_left = $cache_time_int - time();
if ( $time_left > 0 ) {
$hours = floor( $time_left / 3600 );
$cache_status = sprintf( 'Fresh (%dh left)', $hours );
} else {
$cache_status = 'Expired';
}
}
$status[] = array(
'component' => $component,
'detected' => $detected,
'version' => $version ? $version : '-',
'analyzed' => $analyzed,
'cache_status' => $cache_status,
'cache_time' => $cache_time,
);
}
return $status;
}
/**
* Returns the slug of the first installed plugin or theme.
*
* Mirrors the slug derivation used by the vulnerability data loaders: the
* folder name, falling back to the text domain for single-file plugins.
*
* @since 5.1.6
*
* @param string $type Either 'plugin' or 'theme'.
*
* @return string Slug, or an empty string when nothing of that type is installed.
*/
function wpvulnerability_debug_first_installed_slug( $type ) {
if ( 'theme' === $type ) {
foreach ( wp_get_themes() as $theme ) {
return $theme->get_stylesheet();
}
return '';
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'get_plugins' ) ) {
return '';
}
foreach ( get_plugins() as $plugin_file => $plugin_data ) {
$folder = explode( '/', $plugin_file );
$slug = trim( (string) $folder[0] );
if ( '' !== $slug && false === strpos( $plugin_file, '/' ) ) {
// Single-file plugin at the plugins root: use the file name.
$slug = basename( $slug, '.php' );
}
if ( '' === $slug && isset( $plugin_data['TextDomain'] ) ) {
$td_raw = $plugin_data['TextDomain'];
$slug = trim( is_scalar( $td_raw ) ? (string) $td_raw : '' );
}
if ( '' !== $slug ) {
return $slug;
}
}
return '';
}
/**
* Tests API connectivity for a specific component.
*
* @since 4.3.0
* @since 5.1.6 Core collapses pre-release versions to their stable milestone,
* and plugins/themes use the real slug-based API routes.
*
* @param string $component The component to test (e.g., 'core', 'plugins', 'php').
*
* @return array<string, mixed> Result array with success status, response data, and timing.
*/
function wpvulnerability_debug_test_api_component( $component ) {
$result = array(
'success' => false,
'component' => $component,
'http_code' => 0,
'response_time' => 0,
'message' => '',
'data_preview' => '',
);
// Validate component.
$valid_components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
if ( ! in_array( $component, $valid_components, true ) ) {
$result['message'] = __( 'Invalid component specified.', 'wpvulnerability' );
return $result;
}
// Build the API URL for the component. Core and software routes take a
// version; the plugin/theme routes take a slug only.
$url = '';
$version = null;
switch ( $component ) {
case 'core':
global $wp_version;
// The API only serves stable milestones: collapse pre-release
// suffixes (e.g. 7.1-alpha-62421) so the check keeps working.
$version = preg_replace( '/-(?:alpha|beta|rc).*$/i', '', trim( (string) $wp_version ) );
if ( ! is_string( $version ) || '' === $version ) {
$result['message'] = __( 'WordPress version not detected for this component.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . 'core/' . wpvulnerability_sanitize_version( $version ) . '/';
break;
case 'plugins':
case 'themes':
$slug = wpvulnerability_debug_first_installed_slug( 'plugins' === $component ? 'plugin' : 'theme' );
if ( '' === $slug ) {
$result['message'] = 'plugins' === $component
? __( 'No plugins detected to test the API endpoint.', 'wpvulnerability' )
: __( 'No themes detected to test the API endpoint.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . ( 'plugins' === $component ? 'plugin/' : 'theme/' ) . sanitize_title( $slug ) . '/';
break;
default:
if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
$version = wpvulnerability_get_software_version( $component );
}
if ( ! $version ) {
$result['message'] = __( 'No local version detected; the API endpoint was not called.', 'wpvulnerability' );
return $result;
}
$url = WPVULNERABILITY_API_HOST . $component . '/' . wpvulnerability_sanitize_version( $version ) . '/';
break;
}
// Execute request with timing.
$start_time = microtime( true );
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
)
);
$end_time = microtime( true );
$result['response_time'] = round( ( $end_time - $start_time ) * 1000, 2 );
// Process response.
if ( is_wp_error( $response ) ) {
$result['message'] = $response->get_error_message();
return $result;
}
$result['http_code'] = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( 200 === $result['http_code'] ) {
$result['success'] = true;
$result['message'] = __( 'API request successful.', 'wpvulnerability' );
// Create a preview of the response data.
$decoded = json_decode( $body, true );
if ( $decoded ) {
$encoded = wp_json_encode( $decoded, JSON_PRETTY_PRINT );
$preview = false !== $encoded ? $encoded : '';
if ( strlen( $preview ) > 500 ) {
$preview = substr( $preview, 0, 500 ) . '...';
}
$result['data_preview'] = $preview;
} else {
$result['data_preview'] = substr( $body, 0, 500 );
}
} else {
$result['message'] = sprintf(
/* translators: %d: HTTP status code */
__( 'API returned HTTP code %d.', 'wpvulnerability' ),
$result['http_code']
);
}
return $result;
}
/**
* Retrieves cron job status information.
*
* @since 4.3.0
*
* @return array<string, array<string, mixed>> Cron status information.
*/
function wpvulnerability_debug_get_cron_status() {
$cron_status = array(
'update_database' => array(
'hook' => 'wpvulnerability_update_database',
'next_run' => null,
'last_run' => null,
'scheduled' => false,
),
'send_notification' => array(
'hook' => 'wpvulnerability_notification',
'next_run' => null,
'last_run' => null,
'scheduled' => false,
),
);
// Check update database cron.
$next_update = wp_next_scheduled( 'wpvulnerability_update_database' );
if ( $next_update ) {
$cron_status['update_database']['next_run'] = $next_update;
$cron_status['update_database']['scheduled'] = true;
}
// Check notification cron.
$next_notification = wp_next_scheduled( 'wpvulnerability_notification' );
if ( $next_notification ) {
$cron_status['send_notification']['next_run'] = $next_notification;
$cron_status['send_notification']['scheduled'] = true;
}
// 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;
}
}
return $cron_status;
}
/**
* Masks sensitive configuration values (webhook URLs, tokens, emails).
*
* Used whenever configuration data is displayed or exported for debugging,
* so shared debug files never contain usable secrets or recipient addresses.
*
* @since 5.1.3
*
* @param mixed $config Plugin configuration.
* @return array<mixed, mixed> Configuration with sensitive values masked.
*/
function wpvulnerability_debug_mask_config( $config ) {
if ( ! is_array( $config ) ) {
return array();
}
$secret_keys = array( 'slack_webhook', 'teams_webhook', 'discord_webhook', 'telegram_bot_token', 'telegram_chat_id' );
foreach ( $secret_keys as $key ) {
if ( isset( $config[ $key ] ) && is_scalar( $config[ $key ] ) && '' !== (string) $config[ $key ] ) {
$value = (string) $config[ $key ];
$config[ $key ] = strlen( $value ) > 8 ? substr( $value, 0, 4 ) . '...' . substr( $value, -4 ) : '...';
}
}
if ( isset( $config['emails'] ) && is_scalar( $config['emails'] ) && '' !== (string) $config['emails'] ) {
$masked = array();
foreach ( explode( ',', (string) $config['emails'] ) as $email ) {
$parts = explode( '@', trim( $email ) );
if ( count( $parts ) < 2 || '' === $parts[1] ) {
$masked[] = '...';
continue;
}
$masked[] = ( '' !== $parts[0] ? substr( $parts[0], 0, 1 ) . '***' : '***' ) . '@' . $parts[1];
}
$config['emails'] = implode( ',', $masked );
}
return $config;
}
/**
* Exports comprehensive debug information as JSON.
*
* @since 4.3.0
*
* @return string JSON-encoded debug information.
*/
function wpvulnerability_debug_export_info() {
$config = is_multisite()
? get_site_option( 'wpvulnerability-config', array() )
: get_option( 'wpvulnerability-config', array() );
$debug_data = array(
'timestamp' => current_time( 'mysql' ),
'system_info' => wpvulnerability_debug_get_system_info(),
'components' => wpvulnerability_debug_get_component_status(),
'configuration' => wpvulnerability_debug_mask_config( $config ),
'cron_status' => wpvulnerability_debug_get_cron_status(),
'vulnerability_counts' => array(),
);
// Add vulnerability counts for each component.
$components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
foreach ( $components as $component ) {
$vulnerable_option = is_multisite()
? get_site_option( 'wpvulnerability-' . $component . '-vulnerable', 0 )
: get_option( 'wpvulnerability-' . $component . '-vulnerable', 0 );
$count = is_numeric( $vulnerable_option ) ? (int) $vulnerable_option : 0;
$debug_data['vulnerability_counts'][ $component ] = $count;
}
$encoded_debug = wp_json_encode( $debug_data, JSON_PRETTY_PRINT );
return false !== $encoded_debug ? $encoded_debug : '';
}
/**
* Clears all plugin caches and transients.
*
* @since 4.3.0
*
* @return bool True if successful, false otherwise.
*/
function wpvulnerability_debug_clear_all_caches() {
$components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
foreach ( $components as $component ) {
$key = 'wpvulnerability_' . $component;
if ( is_multisite() ) {
delete_site_transient( $key );
delete_site_option( 'wpvulnerability-' . $component . '-cache' );
} else {
delete_transient( $key );
delete_option( 'wpvulnerability-' . $component . '-cache' );
}
}
// Plugin data timestamp option, written by wpvulnerability_plugin_get_data().
if ( is_multisite() ) {
delete_site_option( 'wpvulnerability-plugins-cache-data' );
} else {
delete_option( 'wpvulnerability-plugins-cache-data' );
}
return true;
}
/**
* Resets plugin signatures (MD5 hashes) for plugins and themes.
*
* @since 4.3.0
*
* @return bool True if successful, false otherwise.
*/
function wpvulnerability_debug_reset_signatures() {
if ( is_multisite() ) {
delete_site_option( 'wpvulnerability-plugins-signature' );
delete_site_option( 'wpvulnerability-themes-signature' );
} else {
delete_option( 'wpvulnerability-plugins-signature' );
delete_option( 'wpvulnerability-themes-signature' );
}
return true;
}
/**
* Retrieves all database options related to WPVulnerability.
*
* @since 4.3.0
*
* @return array<int, string> Array of option names.
*/
function wpvulnerability_debug_get_option_names() {
$options = array(
'wpvulnerability-config',
'wpvulnerability-analyze',
'wpvulnerability-statistics',
'wpvulnerability-logs',
);
$components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
foreach ( $components as $component ) {
$options[] = 'wpvulnerability-' . $component;
$options[] = 'wpvulnerability-' . $component . '-cache';
$options[] = 'wpvulnerability-' . $component . '-version';
$options[] = 'wpvulnerability-' . $component . '-vulnerable';
}
$options[] = 'wpvulnerability-plugins-signature';
$options[] = 'wpvulnerability-themes-signature';
return $options;
}
/**
* Retrieves the value of a specific WPVulnerability option.
*
* @since 4.3.0
*
* @param string $option_name The option name to retrieve.
*
* @return mixed|null The option value or null if not found.
*/
function wpvulnerability_debug_get_option_value( $option_name ) {
if ( is_multisite() ) {
$value = get_site_option( $option_name, null );
} else {
$value = get_option( $option_name, null );
}
// Mask secrets when displaying the configuration in the options viewer.
if ( 'wpvulnerability-config' === $option_name && is_array( $value ) ) {
$value = wpvulnerability_debug_mask_config( $value );
}
return $value;
}
/**
* Returns the PHP extensions WordPress makes use of, grouped by relevance.
*
* Purely informational: presence or absence of an extension is neither good
* nor bad. Grouping and notes follow the WordPress core recommendations.
*
* @since 5.1.6
*
* @return array<array{label: string, extensions: array<string, string>}> Each group has a
* label and a map of extension name to description.
*/
function wpvulnerability_debug_get_php_extensions() {
return array(
array(
'label' => __( 'Built-in (no hosting action required)', 'wpvulnerability' ),
'extensions' => array(
'pcre' => __( 'Regular expression engine used throughout PHP and WordPress.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Required', 'wpvulnerability' ),
'extensions' => array(
'hash' => __( 'Hashing, including passwords and update packages.', 'wpvulnerability' ),
'json' => __( 'Communications with other servers and JSON data processing.', 'wpvulnerability' ),
'mysqli' => __( 'Connects to MySQL/MariaDB for database interactions.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Highly recommended', 'wpvulnerability' ),
'extensions' => array(
'curl' => __( 'Performs remote request operations.', 'wpvulnerability' ),
'dom' => __( 'Validates Text Widget content and configures IIS7+ automatically.', 'wpvulnerability' ),
'exif' => __( 'Works with metadata stored in images.', 'wpvulnerability' ),
'fileinfo' => __( 'Detects MIME types of file uploads.', 'wpvulnerability' ),
'igbinary' => __( 'Drop-in replacement for the standard PHP serializer; improves performance.', 'wpvulnerability' ),
'imagick' => __( 'Better image quality for media uploads and PDF thumbnail support.', 'wpvulnerability' ),
'intl' => __( 'Locale-aware operations: formatting, transliteration, collation, timezones.', 'wpvulnerability' ),
'mbstring' => __( 'Properly handles UTF-8 text.', 'wpvulnerability' ),
'openssl' => __( 'SSL-based connections to other hosts.', 'wpvulnerability' ),
'xml' => __( 'XML parsing, such as from a third-party site.', 'wpvulnerability' ),
'zip' => __( 'Decompresses Plugin, Theme, and WordPress update packages.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Object cache (only one is needed)', 'wpvulnerability' ),
'extensions' => array(
'apcu' => __( 'In-memory key-value store for PHP.', 'wpvulnerability' ),
'memcached' => __( 'Distributed memory object caching system.', 'wpvulnerability' ),
'opcache' => __( 'Stores precompiled PHP bytecode to reduce repeated parsing and compilation.', 'wpvulnerability' ),
'redis' => __( 'PHP extension for interfacing with Redis.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Optional improvements', 'wpvulnerability' ),
'extensions' => array(
'timezonedb' => __( 'Timezone database used by the PHP date and time functions.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'Fallbacks / situational use', 'wpvulnerability' ),
'extensions' => array(
'bcmath' => __( 'Arbitrary precision mathematics.', 'wpvulnerability' ),
'filter' => __( 'Securely filters user input.', 'wpvulnerability' ),
'gd' => __( 'Functionally limited fallback for image manipulation when Imagick is unavailable.', 'wpvulnerability' ),
'iconv' => __( 'Converts between character sets.', 'wpvulnerability' ),
'shmop' => __( 'Reads, writes, creates and deletes Unix shared memory segments.', 'wpvulnerability' ),
'simplexml' => __( 'XML parsing.', 'wpvulnerability' ),
'sodium' => __( 'Signature validation and securely random bytes.', 'wpvulnerability' ),
'xmlreader' => __( 'XML parsing.', 'wpvulnerability' ),
'zlib' => __( 'Gzip compression and decompression.', 'wpvulnerability' ),
),
),
array(
'label' => __( 'File changes when files are not writeable (transports)', 'wpvulnerability' ),
'extensions' => array(
'ssh2' => __( 'Shell, remote execution, tunneling and file transfer over SSH.', 'wpvulnerability' ),
'ftp' => __( 'FTP client access for updates and installations.', 'wpvulnerability' ),
'sockets' => __( 'Low-level socket communication functions.', 'wpvulnerability' ),
),
),
);
}
/**
* Detects whether a PHP extension is loaded.
*
* Handles special cases where the internal extension name differs from the
* common name or where availability depends on functions instead.
*
* @since 5.1.6
*
* @param string $extension Extension name as listed in the recommendations.
*
* @return bool True when the extension is loaded.
*/
function wpvulnerability_debug_extension_loaded( $extension ) {
if ( 'opcache' === $extension ) {
return extension_loaded( 'opcache' )
|| extension_loaded( 'Zend OPcache' )
|| function_exists( 'opcache_get_status' );
}
return extension_loaded( $extension );
}
/**
* Gathers system package information relevant to WordPress.
*
* Informational only. Shell probes honour the plugin shell-exec policy
* (security mode, disable constant) through the safe wrapper; when probing
* is not possible the version is reported as unknown.
*
* @since 5.1.6
*
* @return array<array{name: string, available: bool, version: string|null}> Package rows.
*/
function wpvulnerability_debug_get_system_packages() {
$packages = array();
// cURL: reuse the shared detector (PHP extension first, then CLI probe).
$curl_version = function_exists( 'wpvulnerability_detect_curl' ) ? wpvulnerability_detect_curl() : null;
$packages[] = array(
'name' => 'curl',
'available' => null !== $curl_version,
'version' => $curl_version,
);
// ImageMagick: reuse the shared detector.
$imagemagick = function_exists( 'wpvulnerability_detect_imagemagick' ) ? wpvulnerability_detect_imagemagick() : array();
$im_raw = isset( $imagemagick['version'] ) ? $imagemagick['version'] : null;
$im_version = is_string( $im_raw ) ? $im_raw : null;
$packages[] = array(
'name' => 'ImageMagick',
'available' => null !== $im_version,
'version' => $im_version,
);
// Ghost Script: enables Imagick/ImageMagick PDF thumbnail generation.
$gs_version = null;
if ( wpvulnerability_can_shell_exec() ) {
$gs_output = wpvulnerability_safe_shell_exec( 'gs', 'gs --version' );
$gs_matches = array();
if ( is_string( $gs_output ) && preg_match( '/(\d+\.\d+(?:\.\d+)?)/', trim( $gs_output ), $gs_matches ) ) {
$gs_version = $gs_matches[1];
}
}
$packages[] = array(
'name' => 'Ghost Script',
'available' => null !== $gs_version,
'version' => $gs_version,
);
// OpenSSL: report the OpenSSL linked into the PHP build.
$openssl_version = null;
if ( defined( 'OPENSSL_VERSION_TEXT' ) && preg_match( '/(?:OpenSSL|LibreSSL|BoringSSL)\s+(\d[\w.]*)/', (string) OPENSSL_VERSION_TEXT, $o_matches ) ) {
$openssl_version = $o_matches[1];
}
$packages[] = array(
'name' => 'OpenSSL',
'available' => defined( 'OPENSSL_VERSION_TEXT' ),
'version' => $openssl_version,
);
// WebP and AVIF support: check Imagick formats, GD, then CLI tools.
foreach (
array(
'WebP' => array(
'format' => 'WEBP',
'gd_key' => 'WebP Support',
'tool' => 'cwebp',
'command' => 'cwebp -version',
),
'AVIF' => array(
'format' => 'AVIF',
'gd_key' => 'AVIF Support',
'tool' => 'avifenc',
'command' => 'avifenc --version',
),
)
as $format_name => $format_check
) {
$supported = false;
$version = null;
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
try {
$imagick = new Imagick();
$supported = in_array( strtoupper( $format_check['format'] ), $imagick->queryFormats( $format_check['format'] ), true );
} catch ( Exception $e ) {
$supported = false;
}
}
if ( ! $supported && extension_loaded( 'gd' ) && function_exists( 'gd_info' ) ) {
$gd_info = gd_info();
$supported = ! empty( $gd_info[ $format_check['gd_key'] ] );
}
if ( ! $supported && wpvulnerability_can_shell_exec() ) {
$tool_output = wpvulnerability_safe_shell_exec( $format_check['tool'], $format_check['command'] );
if ( is_string( $tool_output ) && '' !== trim( $tool_output ) ) {
$supported = true;
$v_matches = array();
if ( preg_match( '/(\d+\.\d+(?:\.\d+)?)/', $tool_output, $v_matches ) ) {
$version = $v_matches[1];
}
}
}
$packages[] = array(
'name' => $format_name,
'available' => $supported,
'version' => $version,
);
}
return $packages;
}
/**
* Renders the Debug tab section: PHP extensions and system packages.
*
* Informational listing only: nothing here is good or bad.
*
* @since 5.1.6
*
* @return void
*/
function wpvulnerability_render_debug_section_php_extensions() {
$groups = wpvulnerability_debug_get_php_extensions();
$packages = wpvulnerability_debug_get_system_packages();
?>
<div class="wpvulnerability-debug-section">
<h3><?php esc_html_e( 'PHP Extensions', 'wpvulnerability' ); ?></h3>
<p>
<?php esc_html_e( 'WordPress core makes use of various PHP extensions when they are available. This list is informational only: a missing extension is neither good nor bad.', 'wpvulnerability' ); ?>
</p>
<?php
foreach ( $groups as $group ) :
?>
<h4><?php echo esc_html( $group['label'] ); ?></h4>
<table class="widefat striped" style="max-width: 720px;">
<tbody>
<?php
foreach ( $group['extensions'] as $extension => $description ) :
$loaded = wpvulnerability_debug_extension_loaded( $extension );
?>
<tr>
<td style="width: 90px;">
<span style="color: <?php echo $loaded ? '#00a32a;' : '#8c8f94;'; ?>"><?php echo $loaded ? esc_html__( 'Loaded', 'wpvulnerability' ) : esc_html__( 'Not loaded', 'wpvulnerability' ); ?></span>
</td>
<td style="width: 110px;"><code><?php echo esc_html( $extension ); ?></code></td>
<td><?php echo esc_html( $description ); ?></td>
</tr>
<?php
endforeach;
?>
</tbody>
</table>
<?php
endforeach;
?>
<h3><?php esc_html_e( 'System Packages', 'wpvulnerability' ); ?></h3>
<p>
<?php esc_html_e( 'System software WordPress can leverage. Informational only.', 'wpvulnerability' ); ?>
</p>
<table class="widefat striped" style="max-width: 720px;">
<tbody>
<?php
foreach ( $packages as $package ) :
?>
<tr>
<td style="width: 90px;">
<span style="color: <?php echo $package['available'] ? '#00a32a;' : '#8c8f94;'; ?>">
<?php echo $package['available'] ? esc_html__( 'Available', 'wpvulnerability' ) : esc_html__( 'Not detected', 'wpvulnerability' ); ?>
</span>
</td>
<td style="width: 110px;"><code><?php echo esc_html( (string) $package['name'] ); ?></code></td>
<td>
<?php
if ( null !== $package['version'] && '' !== (string) $package['version'] ) {
echo esc_html(
sprintf(
/* translators: %s: package version number */
__( 'Version %s', 'wpvulnerability' ),
(string) $package['version']
)
);
} elseif ( ! $package['available'] ) {
esc_html_e( 'No local version detected.', 'wpvulnerability' );
}
?>
</td>
</tr>
<?php
endforeach;
?>
</tbody>
</table>
</div>
<?php
}