PluginProbe
WPVulnerability / 5.0.0
WPVulnerability v5.0.0
5.1.6 5.1.2 5.1.1 5.0.1 5.0.0 trunk 0.1 0.2 1.0 1.0.1 1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 57 releases
wpvulnerability / wpvulnerability-debug.php

wpvulnerability-debug.php in WPVulnerability 5.0.0, at wpvulnerability-debug.php

662 lines 19.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Debug functions
4 *
5 * @package WPVulnerability
6 *
7 * @version 4.3.0
8 */
9
10 defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
11
12 // Load required plugin files if not already loaded.
13 if ( ! function_exists( 'wpvulnerability_analyze_filter' ) ) {
14 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-general.php';
15 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-run.php';
16 }
17
18 if ( ! function_exists( 'wpvulnerability_get_software_version' ) ) {
19 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-software.php';
20 }
21
22 /**
23 * Retrieves debug log file information.
24 *
25 * Detects the location of the WordPress debug log file and checks if it's accessible.
26 *
27 * @since 4.3.0
28 *
29 * @return array<string, mixed> Array with 'path', 'exists', 'size', 'url', and 'accessible' keys.
30 */
31 function wpvulnerability_debug_get_log_file_info() {
32 $log_info = array(
33 'path' => null,
34 'exists' => false,
35 'size' => 0,
36 'url' => null,
37 'accessible' => false,
38 );
39
40 // Check if WP_DEBUG_LOG is enabled.
41 if ( ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) {
42 return $log_info;
43 }
44
45 // Determine log file path.
46 // WP_DEBUG_LOG can be a string (custom path) or true/false.
47 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_constant -- need runtime value for type narrowing
48 $wp_debug_log_value = defined( 'WP_DEBUG_LOG' ) ? WP_DEBUG_LOG : false;
49 if ( is_string( $wp_debug_log_value ) ) { // @phpstan-ignore function.impossibleType (WP_DEBUG_LOG may be a string path at runtime)
50 // Custom path specified.
51 $log_file = $wp_debug_log_value;
52 } else {
53 // Default path: wp-content/debug.log.
54 $log_file = WP_CONTENT_DIR . '/debug.log';
55 }
56
57 $log_info['path'] = $log_file;
58
59 // Check if file exists.
60 if ( file_exists( $log_file ) ) {
61 $log_info['exists'] = true;
62
63 // Get file size.
64 $size = filesize( $log_file );
65 if ( false !== $size ) {
66 $log_info['size'] = $size;
67 }
68
69 // Check if file is within wp-content (accessible via web).
70 $wp_content_dir = realpath( WP_CONTENT_DIR );
71 $log_file_real = realpath( $log_file );
72
73 if ( $wp_content_dir && $log_file_real && 0 === strpos( $log_file_real, $wp_content_dir ) ) {
74 // File is within wp-content, generate URL.
75 $relative_path = str_replace( $wp_content_dir, '', $log_file_real );
76 $relative_path = str_replace( '\\', '/', $relative_path );
77 $log_info['url'] = content_url() . $relative_path;
78 $log_info['accessible'] = true;
79 }
80 }
81
82 return $log_info;
83 }
84
85 /**
86 * Enhanced web server detection for debug purposes.
87 *
88 * Detects a wider range of web servers including nginx, Apache, LiteSpeed,
89 * Caddy, IIS, Angie, OpenLiteSpeed, and others.
90 *
91 * @since 4.3.0
92 *
93 * @return array<string, string> Array with 'name' and 'version' keys.
94 */
95 function wpvulnerability_debug_detect_webserver() {
96 $webserver = array(
97 'name' => 'Unknown',
98 'version' => '',
99 );
100
101 // First try the plugin's standard detection.
102 $detected = wpvulnerability_detect_webserver();
103 if ( ! empty( $detected['name'] ) ) {
104 $webserver['name'] = $detected['name'];
105 if ( ! empty( $detected['version'] ) ) {
106 $webserver['version'] = $detected['version'];
107 }
108 }
109
110 // If still unknown, try enhanced detection from SERVER_SOFTWARE.
111 if ( 'Unknown' === $webserver['name'] && isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
112 $server_software = sanitize_text_field( wp_unslash( is_string( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '' ) );
113 $server_lower = strtolower( $server_software );
114
115 // LiteSpeed detection.
116 if ( false !== stripos( $server_lower, 'litespeed' ) ) {
117 if ( preg_match( '/litespeed/i', $server_software, $match ) ) {
118 $webserver['name'] = 'LiteSpeed';
119 if ( preg_match( '/litespeed\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
120 $webserver['version'] = $version_match[1];
121 }
122 }
123 }
124
125 // OpenLiteSpeed detection.
126 if ( false !== stripos( $server_lower, 'openlitespeed' ) ) {
127 $webserver['name'] = 'OpenLiteSpeed';
128 if ( preg_match( '/openlitespeed\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
129 $webserver['version'] = $version_match[1];
130 }
131 }
132
133 // Caddy detection.
134 if ( false !== stripos( $server_lower, 'caddy' ) ) {
135 $webserver['name'] = 'Caddy';
136 if ( preg_match( '/caddy\/?v?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
137 $webserver['version'] = $version_match[1];
138 }
139 }
140
141 // IIS detection.
142 if ( false !== stripos( $server_lower, 'microsoft-iis' ) || false !== stripos( $server_lower, 'iis' ) ) {
143 $webserver['name'] = 'Microsoft IIS';
144 if ( preg_match( '/(?:microsoft-)?iis\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
145 $webserver['version'] = $version_match[1];
146 }
147 }
148
149 // Angie detection (nginx fork).
150 if ( false !== stripos( $server_lower, 'angie' ) ) {
151 $webserver['name'] = 'Angie';
152 if ( preg_match( '/angie\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
153 $webserver['version'] = $version_match[1];
154 }
155 }
156
157 // OpenResty detection (nginx-based).
158 if ( false !== stripos( $server_lower, 'openresty' ) ) {
159 $webserver['name'] = 'OpenResty';
160 if ( preg_match( '/openresty\/?(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)/i', $server_software, $version_match ) ) {
161 $webserver['version'] = $version_match[1];
162 }
163 }
164
165 // Tengine detection (nginx fork).
166 if ( false !== stripos( $server_lower, 'tengine' ) ) {
167 $webserver['name'] = 'Tengine';
168 if ( preg_match( '/tengine\/?(\d+\.\d+(?:\.\d+)?)/i', $server_software, $version_match ) ) {
169 $webserver['version'] = $version_match[1];
170 }
171 }
172 }
173
174 // Try shell commands for additional detection if shell_exec is allowed.
175 if ( 'Unknown' === $webserver['name'] && function_exists( 'wpvulnerability_safe_shell_exec' ) ) {
176 // Try LiteSpeed.
177 $litespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which litespeed 2>/dev/null' );
178 if ( ! empty( $litespeed_test ) ) {
179 $webserver['name'] = 'LiteSpeed';
180 }
181
182 // Try OpenLiteSpeed.
183 $openlitespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which openlitespeed 2>/dev/null' );
184 if ( ! empty( $openlitespeed_test ) ) {
185 $webserver['name'] = 'OpenLiteSpeed';
186 }
187
188 // Try Caddy.
189 $caddy_version = wpvulnerability_safe_shell_exec( 'apache', 'caddy version 2>/dev/null' );
190 if ( ! empty( $caddy_version ) && preg_match( '/v?(\d+\.\d+\.\d+)/', $caddy_version, $version_match ) ) {
191 $webserver['name'] = 'Caddy';
192 $webserver['version'] = $version_match[1];
193 }
194 }
195
196 return $webserver;
197 }
198
199 /**
200 * Retrieves comprehensive system information for debugging.
201 *
202 * @since 4.3.0
203 *
204 * @return array<string, mixed> Associative array containing system information.
205 */
206 function wpvulnerability_debug_get_system_info() {
207 global $wp_version, $wpdb;
208
209 // Detect database type (MariaDB vs MySQL).
210 $sqlserver = wpvulnerability_detect_sqlserver();
211 $db_type = ! empty( $sqlserver['name'] ) ? $sqlserver['name'] : 'MySQL';
212 $db_version = ! empty( $sqlserver['version'] ) ? $sqlserver['version'] : $wpdb->db_version();
213
214 // Detect web server with enhanced detection.
215 $webserver = wpvulnerability_debug_detect_webserver();
216 $webserver_name = $webserver['name'];
217 if ( ! empty( $webserver['version'] ) ) {
218 $webserver_name .= ' ' . $webserver['version'];
219 }
220
221 $info = array(
222 'wordpress' => array(
223 'version' => $wp_version,
224 'multisite' => is_multisite(),
225 'language' => get_locale(),
226 'home_url' => home_url(),
227 'site_url' => site_url(),
228 ),
229 'php' => array(
230 'version' => phpversion(),
231 'extensions' => array(
232 'curl' => extension_loaded( 'curl' ),
233 'json' => extension_loaded( 'json' ),
234 'mbstring' => extension_loaded( 'mbstring' ),
235 'xml' => extension_loaded( 'xml' ),
236 'zip' => extension_loaded( 'zip' ),
237 ),
238 'memory' => array(
239 'limit' => ini_get( 'memory_limit' ),
240 'usage' => size_format( memory_get_usage( true ) ),
241 'peak' => size_format( memory_get_peak_usage( true ) ),
242 ),
243 ),
244 'database' => array(
245 'type' => $db_type,
246 'version' => $db_version,
247 ),
248 'webserver' => array(
249 'software' => $webserver_name,
250 ),
251 'debug' => array(
252 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
253 'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
254 'wp_debug_display' => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
255 'script_debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
256 'log_file' => wpvulnerability_debug_get_log_file_info(),
257 ),
258 'plugin' => array(
259 'version' => WPVULNERABILITY_PLUGIN_VERSION,
260 'path' => WPVULNERABILITY_PLUGIN_PATH,
261 ),
262 );
263
264 return $info;
265 }
266
267 /**
268 * Retrieves the status of all trackable components.
269 *
270 * @since 4.3.0
271 *
272 * @return array<int, array<string, mixed>> Array of component statuses.
273 */
274 function wpvulnerability_debug_get_component_status() {
275 $components = array(
276 'core',
277 'plugins',
278 'themes',
279 'php',
280 'apache',
281 'nginx',
282 'mysql',
283 'mariadb',
284 'imagemagick',
285 'curl',
286 'memcached',
287 'redis',
288 'sqlite',
289 );
290
291 $status = array();
292
293 foreach ( $components as $component ) {
294 $version = null;
295 $detected = false;
296 $analyzed = wpvulnerability_analyze_filter( $component );
297 $cache_time = is_multisite()
298 ? get_site_option( 'wpvulnerability-' . $component . '-cache' )
299 : get_option( 'wpvulnerability-' . $component . '-cache' );
300
301 // Decode cache time if it's JSON-encoded.
302 if ( $cache_time && is_string( $cache_time ) ) {
303 $decoded = json_decode( $cache_time );
304 if ( null !== $decoded ) {
305 $cache_time = $decoded;
306 }
307 }
308
309 // Determine detection status and version.
310 switch ( $component ) {
311 case 'core':
312 global $wp_version;
313 $version = $wp_version;
314 $detected = true;
315 break;
316
317 case 'plugins':
318 if ( ! function_exists( 'get_plugins' ) ) {
319 require_once ABSPATH . 'wp-admin/includes/plugin.php';
320 }
321 $all_plugins = get_plugins();
322 $version = count( $all_plugins ) . ' installed';
323 $detected = true;
324 break;
325
326 case 'themes':
327 $all_themes = wp_get_themes();
328 $version = count( $all_themes ) . ' installed';
329 $detected = true;
330 break;
331
332 case 'php':
333 case 'apache':
334 case 'nginx':
335 case 'mysql':
336 case 'mariadb':
337 case 'imagemagick':
338 case 'curl':
339 case 'memcached':
340 case 'redis':
341 case 'sqlite':
342 if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
343 $version = wpvulnerability_get_software_version( $component );
344 if ( null !== $version ) {
345 $detected = true;
346 }
347 }
348 break;
349 }
350
351 // Calculate cache status.
352 $cache_status = 'No cache';
353 if ( $cache_time && is_numeric( $cache_time ) ) {
354 $cache_time_int = (int) $cache_time;
355 $time_left = $cache_time_int - time();
356 if ( $time_left > 0 ) {
357 $hours = floor( $time_left / 3600 );
358 $cache_status = sprintf( 'Fresh (%dh left)', $hours );
359 } else {
360 $cache_status = 'Expired';
361 }
362 }
363
364 $status[] = array(
365 'component' => $component,
366 'detected' => $detected,
367 'version' => $version ? $version : '-',
368 'analyzed' => $analyzed,
369 'cache_status' => $cache_status,
370 'cache_time' => $cache_time,
371 );
372 }
373
374 return $status;
375 }
376
377 /**
378 * Tests API connectivity for a specific component.
379 *
380 * @since 4.3.0
381 *
382 * @param string $component The component to test (e.g., 'core', 'plugins', 'php').
383 *
384 * @return array<string, mixed> Result array with success status, response data, and timing.
385 */
386 function wpvulnerability_debug_test_api_component( $component ) {
387 $result = array(
388 'success' => false,
389 'component' => $component,
390 'http_code' => 0,
391 'response_time' => 0,
392 'message' => '',
393 'data_preview' => '',
394 );
395
396 // Validate component.
397 $valid_components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
398 if ( ! in_array( $component, $valid_components, true ) ) {
399 $result['message'] = __( 'Invalid component specified.', 'wpvulnerability' );
400 return $result;
401 }
402
403 // Get version for the component.
404 $version = null;
405 switch ( $component ) {
406 case 'core':
407 global $wp_version;
408 $version = $wp_version;
409 break;
410 case 'plugins':
411 case 'themes':
412 // Use a generic version for testing.
413 $version = '1.0.0';
414 break;
415 default:
416 if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
417 $version = wpvulnerability_get_software_version( $component );
418 }
419 break;
420 }
421
422 if ( ! $version ) {
423 $result['message'] = __( 'Version not detected for this component.', 'wpvulnerability' );
424 return $result;
425 }
426
427 // Build API URL.
428 $url = WPVULNERABILITY_API_HOST . $component . '/' . $version . '/';
429
430 // Execute request with timing.
431 $start_time = microtime( true );
432 $response = wp_remote_get(
433 $url,
434 array(
435 'timeout' => 10,
436 )
437 );
438 $end_time = microtime( true );
439
440 $result['response_time'] = round( ( $end_time - $start_time ) * 1000, 2 );
441
442 // Process response.
443 if ( is_wp_error( $response ) ) {
444 $result['message'] = $response->get_error_message();
445 return $result;
446 }
447
448 $result['http_code'] = wp_remote_retrieve_response_code( $response );
449 $body = wp_remote_retrieve_body( $response );
450
451 if ( 200 === $result['http_code'] ) {
452 $result['success'] = true;
453 $result['message'] = __( 'API request successful.', 'wpvulnerability' );
454
455 // Create a preview of the response data.
456 $decoded = json_decode( $body, true );
457 if ( $decoded ) {
458 $encoded = wp_json_encode( $decoded, JSON_PRETTY_PRINT );
459 $preview = false !== $encoded ? $encoded : '';
460 if ( strlen( $preview ) > 500 ) {
461 $preview = substr( $preview, 0, 500 ) . '...';
462 }
463 $result['data_preview'] = $preview;
464 } else {
465 $result['data_preview'] = substr( $body, 0, 500 );
466 }
467 } else {
468 $result['message'] = sprintf(
469 /* translators: %d: HTTP status code */
470 __( 'API returned HTTP code %d.', 'wpvulnerability' ),
471 $result['http_code']
472 );
473 }
474
475 return $result;
476 }
477
478 /**
479 * Retrieves cron job status information.
480 *
481 * @since 4.3.0
482 *
483 * @return array<string, array<string, mixed>> Cron status information.
484 */
485 function wpvulnerability_debug_get_cron_status() {
486 $cron_status = array(
487 'update_database' => array(
488 'hook' => 'wpvulnerability_update_database',
489 'next_run' => null,
490 'last_run' => null,
491 'scheduled' => false,
492 ),
493 'send_notification' => array(
494 'hook' => 'wpvulnerability_send_notification',
495 'next_run' => null,
496 'last_run' => null,
497 'scheduled' => false,
498 ),
499 );
500
501 // Check update database cron.
502 $next_update = wp_next_scheduled( 'wpvulnerability_update_database' );
503 if ( $next_update ) {
504 $cron_status['update_database']['next_run'] = $next_update;
505 $cron_status['update_database']['scheduled'] = true;
506 }
507
508 // Check notification cron.
509 $next_notification = wp_next_scheduled( 'wpvulnerability_send_notification' );
510 if ( $next_notification ) {
511 $cron_status['send_notification']['next_run'] = $next_notification;
512 $cron_status['send_notification']['scheduled'] = true;
513 }
514
515 // Try to get last run times from logs.
516 $logs_raw = is_multisite() ? get_site_option( 'wpvulnerability-logs', array() ) : get_option( 'wpvulnerability-logs', array() );
517 $logs = is_array( $logs_raw ) ? $logs_raw : array();
518 if ( ! empty( $logs ) ) {
519 // Get the most recent log entry for each type.
520 foreach ( array_reverse( $logs ) as $log ) {
521 if ( ! is_array( $log ) ) {
522 continue; }
523 if ( isset( $log['time'] ) && isset( $log['url'] ) ) {
524 $timestamp = $log['time'];
525
526 // Check if this is an update-related log.
527 if ( ! $cron_status['update_database']['last_run'] ) {
528 $cron_status['update_database']['last_run'] = $timestamp;
529 }
530 }
531 }
532 }
533
534 return $cron_status;
535 }
536
537 /**
538 * Exports comprehensive debug information as JSON.
539 *
540 * @since 4.3.0
541 *
542 * @return string JSON-encoded debug information.
543 */
544 function wpvulnerability_debug_export_info() {
545 $config = is_multisite()
546 ? get_site_option( 'wpvulnerability-config', array() )
547 : get_option( 'wpvulnerability-config', array() );
548
549 $debug_data = array(
550 'timestamp' => current_time( 'mysql' ),
551 'system_info' => wpvulnerability_debug_get_system_info(),
552 'components' => wpvulnerability_debug_get_component_status(),
553 'configuration' => $config,
554 'cron_status' => wpvulnerability_debug_get_cron_status(),
555 'vulnerability_counts' => array(),
556 );
557
558 // Add vulnerability counts for each component.
559 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
560 foreach ( $components as $component ) {
561 $vulnerable_option = is_multisite()
562 ? get_site_option( 'wpvulnerability-' . $component . '-vulnerable', 0 )
563 : get_option( 'wpvulnerability-' . $component . '-vulnerable', 0 );
564 $count = is_numeric( $vulnerable_option ) ? (int) $vulnerable_option : 0;
565 $debug_data['vulnerability_counts'][ $component ] = $count;
566 }
567
568 $encoded_debug = wp_json_encode( $debug_data, JSON_PRETTY_PRINT );
569 return false !== $encoded_debug ? $encoded_debug : '';
570 }
571
572 /**
573 * Clears all plugin caches and transients.
574 *
575 * @since 4.3.0
576 *
577 * @return bool True if successful, false otherwise.
578 */
579 function wpvulnerability_debug_clear_all_caches() {
580 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
581
582 foreach ( $components as $component ) {
583 $key = 'wpvulnerability_' . $component;
584
585 if ( is_multisite() ) {
586 delete_site_transient( $key );
587 delete_site_option( 'wpvulnerability-' . $component . '-cache' );
588 } else {
589 delete_transient( $key );
590 delete_option( 'wpvulnerability-' . $component . '-cache' );
591 }
592 }
593
594 return true;
595 }
596
597 /**
598 * Resets plugin signatures (MD5 hashes) for plugins and themes.
599 *
600 * @since 4.3.0
601 *
602 * @return bool True if successful, false otherwise.
603 */
604 function wpvulnerability_debug_reset_signatures() {
605 if ( is_multisite() ) {
606 delete_site_option( 'wpvulnerability-plugins-signature' );
607 delete_site_option( 'wpvulnerability-themes-signature' );
608 } else {
609 delete_option( 'wpvulnerability-plugins-signature' );
610 delete_option( 'wpvulnerability-themes-signature' );
611 }
612
613 return true;
614 }
615
616 /**
617 * Retrieves all database options related to WPVulnerability.
618 *
619 * @since 4.3.0
620 *
621 * @return array<int, string> Array of option names.
622 */
623 function wpvulnerability_debug_get_option_names() {
624 $options = array(
625 'wpvulnerability-config',
626 'wpvulnerability-analyze',
627 'wpvulnerability-statistics',
628 'wpvulnerability-logs',
629 );
630
631 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
632
633 foreach ( $components as $component ) {
634 $options[] = 'wpvulnerability-' . $component;
635 $options[] = 'wpvulnerability-' . $component . '-cache';
636 $options[] = 'wpvulnerability-' . $component . '-version';
637 $options[] = 'wpvulnerability-' . $component . '-vulnerable';
638 }
639
640 $options[] = 'wpvulnerability-plugins-signature';
641 $options[] = 'wpvulnerability-themes-signature';
642
643 return $options;
644 }
645
646 /**
647 * Retrieves the value of a specific WPVulnerability option.
648 *
649 * @since 4.3.0
650 *
651 * @param string $option_name The option name to retrieve.
652 *
653 * @return mixed|null The option value or null if not found.
654 */
655 function wpvulnerability_debug_get_option_value( $option_name ) {
656 if ( is_multisite() ) {
657 return get_site_option( $option_name, null );
658 } else {
659 return get_option( $option_name, null );
660 }
661 }
662