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 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 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> 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; } /** * Tests API connectivity for a specific component. * * @since 4.3.0 * * @param string $component The component to test (e.g., 'core', 'plugins', 'php'). * * @return array 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; } // Get version for the component. $version = null; switch ( $component ) { case 'core': global $wp_version; $version = $wp_version; break; case 'plugins': case 'themes': // Use a generic version for testing. $version = '1.0.0'; break; default: if ( function_exists( 'wpvulnerability_get_software_version' ) ) { $version = wpvulnerability_get_software_version( $component ); } break; } if ( ! $version ) { $result['message'] = __( 'Version not detected for this component.', 'wpvulnerability' ); return $result; } // Build API URL. $url = WPVULNERABILITY_API_HOST . $component . '/' . $version . '/'; // 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> 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; } /** * 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' => $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' ); } } 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 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() ) { return get_site_option( $option_name, null ); } else { return get_option( $option_name, null ); } }