PluginProbe
WPVulnerability / 5.1.2
WPVulnerability v5.1.2
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.1.2, at wpvulnerability-debug.php

665 lines 19.5 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. `which` prints the binary path on success; on failure it
177 // prints a "which: no litespeed ..." message to stderr. The wrapper merges
178 // stderr into stdout (2>&1), so validate that the output is actually a path.
179 $litespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which litespeed' );
180 if ( ! empty( $litespeed_test ) && 0 === strpos( trim( (string) $litespeed_test ), '/' ) ) {
181 $webserver['name'] = 'LiteSpeed';
182 }
183
184 // Try OpenLiteSpeed.
185 $openlitespeed_test = wpvulnerability_safe_shell_exec( 'apache', 'which openlitespeed' );
186 if ( ! empty( $openlitespeed_test ) && 0 === strpos( trim( (string) $openlitespeed_test ), '/' ) ) {
187 $webserver['name'] = 'OpenLiteSpeed';
188 }
189
190 // Try Caddy. The version regex guards against false positives: a "command
191 // not found" message does not match a version pattern.
192 $caddy_version = wpvulnerability_safe_shell_exec( 'apache', 'caddy version' );
193 if ( ! empty( $caddy_version ) && preg_match( '/v?(\d+\.\d+\.\d+)/', $caddy_version, $version_match ) ) {
194 $webserver['name'] = 'Caddy';
195 $webserver['version'] = $version_match[1];
196 }
197 }
198
199 return $webserver;
200 }
201
202 /**
203 * Retrieves comprehensive system information for debugging.
204 *
205 * @since 4.3.0
206 *
207 * @return array<string, mixed> Associative array containing system information.
208 */
209 function wpvulnerability_debug_get_system_info() {
210 global $wp_version, $wpdb;
211
212 // Detect database type (MariaDB vs MySQL).
213 $sqlserver = wpvulnerability_detect_sqlserver();
214 $db_type = ! empty( $sqlserver['name'] ) ? $sqlserver['name'] : 'MySQL';
215 $db_version = ! empty( $sqlserver['version'] ) ? $sqlserver['version'] : $wpdb->db_version();
216
217 // Detect web server with enhanced detection.
218 $webserver = wpvulnerability_debug_detect_webserver();
219 $webserver_name = $webserver['name'];
220 if ( ! empty( $webserver['version'] ) ) {
221 $webserver_name .= ' ' . $webserver['version'];
222 }
223
224 $info = array(
225 'wordpress' => array(
226 'version' => $wp_version,
227 'multisite' => is_multisite(),
228 'language' => get_locale(),
229 'home_url' => home_url(),
230 'site_url' => site_url(),
231 ),
232 'php' => array(
233 'version' => phpversion(),
234 'extensions' => array(
235 'curl' => extension_loaded( 'curl' ),
236 'json' => extension_loaded( 'json' ),
237 'mbstring' => extension_loaded( 'mbstring' ),
238 'xml' => extension_loaded( 'xml' ),
239 'zip' => extension_loaded( 'zip' ),
240 ),
241 'memory' => array(
242 'limit' => ini_get( 'memory_limit' ),
243 'usage' => size_format( memory_get_usage( true ) ),
244 'peak' => size_format( memory_get_peak_usage( true ) ),
245 ),
246 ),
247 'database' => array(
248 'type' => $db_type,
249 'version' => $db_version,
250 ),
251 'webserver' => array(
252 'software' => $webserver_name,
253 ),
254 'debug' => array(
255 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
256 'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
257 'wp_debug_display' => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
258 'script_debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
259 'log_file' => wpvulnerability_debug_get_log_file_info(),
260 ),
261 'plugin' => array(
262 'version' => WPVULNERABILITY_PLUGIN_VERSION,
263 'path' => WPVULNERABILITY_PLUGIN_PATH,
264 ),
265 );
266
267 return $info;
268 }
269
270 /**
271 * Retrieves the status of all trackable components.
272 *
273 * @since 4.3.0
274 *
275 * @return array<int, array<string, mixed>> Array of component statuses.
276 */
277 function wpvulnerability_debug_get_component_status() {
278 $components = array(
279 'core',
280 'plugins',
281 'themes',
282 'php',
283 'apache',
284 'nginx',
285 'mysql',
286 'mariadb',
287 'imagemagick',
288 'curl',
289 'memcached',
290 'redis',
291 'sqlite',
292 );
293
294 $status = array();
295
296 foreach ( $components as $component ) {
297 $version = null;
298 $detected = false;
299 $analyzed = wpvulnerability_analyze_filter( $component );
300 $cache_time = is_multisite()
301 ? get_site_option( 'wpvulnerability-' . $component . '-cache' )
302 : get_option( 'wpvulnerability-' . $component . '-cache' );
303
304 // Decode cache time if it's JSON-encoded.
305 if ( $cache_time && is_string( $cache_time ) ) {
306 $decoded = json_decode( $cache_time );
307 if ( null !== $decoded ) {
308 $cache_time = $decoded;
309 }
310 }
311
312 // Determine detection status and version.
313 switch ( $component ) {
314 case 'core':
315 global $wp_version;
316 $version = $wp_version;
317 $detected = true;
318 break;
319
320 case 'plugins':
321 if ( ! function_exists( 'get_plugins' ) ) {
322 require_once ABSPATH . 'wp-admin/includes/plugin.php';
323 }
324 $all_plugins = get_plugins();
325 $version = count( $all_plugins ) . ' installed';
326 $detected = true;
327 break;
328
329 case 'themes':
330 $all_themes = wp_get_themes();
331 $version = count( $all_themes ) . ' installed';
332 $detected = true;
333 break;
334
335 case 'php':
336 case 'apache':
337 case 'nginx':
338 case 'mysql':
339 case 'mariadb':
340 case 'imagemagick':
341 case 'curl':
342 case 'memcached':
343 case 'redis':
344 case 'sqlite':
345 if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
346 $version = wpvulnerability_get_software_version( $component );
347 if ( null !== $version ) {
348 $detected = true;
349 }
350 }
351 break;
352 }
353
354 // Calculate cache status.
355 $cache_status = 'No cache';
356 if ( $cache_time && is_numeric( $cache_time ) ) {
357 $cache_time_int = (int) $cache_time;
358 $time_left = $cache_time_int - time();
359 if ( $time_left > 0 ) {
360 $hours = floor( $time_left / 3600 );
361 $cache_status = sprintf( 'Fresh (%dh left)', $hours );
362 } else {
363 $cache_status = 'Expired';
364 }
365 }
366
367 $status[] = array(
368 'component' => $component,
369 'detected' => $detected,
370 'version' => $version ? $version : '-',
371 'analyzed' => $analyzed,
372 'cache_status' => $cache_status,
373 'cache_time' => $cache_time,
374 );
375 }
376
377 return $status;
378 }
379
380 /**
381 * Tests API connectivity for a specific component.
382 *
383 * @since 4.3.0
384 *
385 * @param string $component The component to test (e.g., 'core', 'plugins', 'php').
386 *
387 * @return array<string, mixed> Result array with success status, response data, and timing.
388 */
389 function wpvulnerability_debug_test_api_component( $component ) {
390 $result = array(
391 'success' => false,
392 'component' => $component,
393 'http_code' => 0,
394 'response_time' => 0,
395 'message' => '',
396 'data_preview' => '',
397 );
398
399 // Validate component.
400 $valid_components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
401 if ( ! in_array( $component, $valid_components, true ) ) {
402 $result['message'] = __( 'Invalid component specified.', 'wpvulnerability' );
403 return $result;
404 }
405
406 // Get version for the component.
407 $version = null;
408 switch ( $component ) {
409 case 'core':
410 global $wp_version;
411 $version = $wp_version;
412 break;
413 case 'plugins':
414 case 'themes':
415 // Use a generic version for testing.
416 $version = '1.0.0';
417 break;
418 default:
419 if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
420 $version = wpvulnerability_get_software_version( $component );
421 }
422 break;
423 }
424
425 if ( ! $version ) {
426 $result['message'] = __( 'Version not detected for this component.', 'wpvulnerability' );
427 return $result;
428 }
429
430 // Build API URL.
431 $url = WPVULNERABILITY_API_HOST . $component . '/' . $version . '/';
432
433 // Execute request with timing.
434 $start_time = microtime( true );
435 $response = wp_remote_get(
436 $url,
437 array(
438 'timeout' => 10,
439 )
440 );
441 $end_time = microtime( true );
442
443 $result['response_time'] = round( ( $end_time - $start_time ) * 1000, 2 );
444
445 // Process response.
446 if ( is_wp_error( $response ) ) {
447 $result['message'] = $response->get_error_message();
448 return $result;
449 }
450
451 $result['http_code'] = wp_remote_retrieve_response_code( $response );
452 $body = wp_remote_retrieve_body( $response );
453
454 if ( 200 === $result['http_code'] ) {
455 $result['success'] = true;
456 $result['message'] = __( 'API request successful.', 'wpvulnerability' );
457
458 // Create a preview of the response data.
459 $decoded = json_decode( $body, true );
460 if ( $decoded ) {
461 $encoded = wp_json_encode( $decoded, JSON_PRETTY_PRINT );
462 $preview = false !== $encoded ? $encoded : '';
463 if ( strlen( $preview ) > 500 ) {
464 $preview = substr( $preview, 0, 500 ) . '...';
465 }
466 $result['data_preview'] = $preview;
467 } else {
468 $result['data_preview'] = substr( $body, 0, 500 );
469 }
470 } else {
471 $result['message'] = sprintf(
472 /* translators: %d: HTTP status code */
473 __( 'API returned HTTP code %d.', 'wpvulnerability' ),
474 $result['http_code']
475 );
476 }
477
478 return $result;
479 }
480
481 /**
482 * Retrieves cron job status information.
483 *
484 * @since 4.3.0
485 *
486 * @return array<string, array<string, mixed>> Cron status information.
487 */
488 function wpvulnerability_debug_get_cron_status() {
489 $cron_status = array(
490 'update_database' => array(
491 'hook' => 'wpvulnerability_update_database',
492 'next_run' => null,
493 'last_run' => null,
494 'scheduled' => false,
495 ),
496 'send_notification' => array(
497 'hook' => 'wpvulnerability_notification',
498 'next_run' => null,
499 'last_run' => null,
500 'scheduled' => false,
501 ),
502 );
503
504 // Check update database cron.
505 $next_update = wp_next_scheduled( 'wpvulnerability_update_database' );
506 if ( $next_update ) {
507 $cron_status['update_database']['next_run'] = $next_update;
508 $cron_status['update_database']['scheduled'] = true;
509 }
510
511 // Check notification cron.
512 $next_notification = wp_next_scheduled( 'wpvulnerability_notification' );
513 if ( $next_notification ) {
514 $cron_status['send_notification']['next_run'] = $next_notification;
515 $cron_status['send_notification']['scheduled'] = true;
516 }
517
518 // Last run = most recent API response log (stored as the wpvulnerability_log CPT).
519 $last_log = get_posts(
520 array(
521 'post_type' => 'wpvulnerability_log',
522 'post_status' => 'any',
523 'posts_per_page' => 1,
524 'orderby' => 'date',
525 'order' => 'DESC',
526 'fields' => 'ids',
527 'no_found_rows' => true,
528 )
529 );
530 if ( ! empty( $last_log ) ) {
531 $timestamp = get_post_timestamp( $last_log[0] );
532 if ( false !== $timestamp ) {
533 $cron_status['update_database']['last_run'] = $timestamp;
534 }
535 }
536
537 return $cron_status;
538 }
539
540 /**
541 * Exports comprehensive debug information as JSON.
542 *
543 * @since 4.3.0
544 *
545 * @return string JSON-encoded debug information.
546 */
547 function wpvulnerability_debug_export_info() {
548 $config = is_multisite()
549 ? get_site_option( 'wpvulnerability-config', array() )
550 : get_option( 'wpvulnerability-config', array() );
551
552 $debug_data = array(
553 'timestamp' => current_time( 'mysql' ),
554 'system_info' => wpvulnerability_debug_get_system_info(),
555 'components' => wpvulnerability_debug_get_component_status(),
556 'configuration' => $config,
557 'cron_status' => wpvulnerability_debug_get_cron_status(),
558 'vulnerability_counts' => array(),
559 );
560
561 // Add vulnerability counts for each component.
562 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
563 foreach ( $components as $component ) {
564 $vulnerable_option = is_multisite()
565 ? get_site_option( 'wpvulnerability-' . $component . '-vulnerable', 0 )
566 : get_option( 'wpvulnerability-' . $component . '-vulnerable', 0 );
567 $count = is_numeric( $vulnerable_option ) ? (int) $vulnerable_option : 0;
568 $debug_data['vulnerability_counts'][ $component ] = $count;
569 }
570
571 $encoded_debug = wp_json_encode( $debug_data, JSON_PRETTY_PRINT );
572 return false !== $encoded_debug ? $encoded_debug : '';
573 }
574
575 /**
576 * Clears all plugin caches and transients.
577 *
578 * @since 4.3.0
579 *
580 * @return bool True if successful, false otherwise.
581 */
582 function wpvulnerability_debug_clear_all_caches() {
583 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
584
585 foreach ( $components as $component ) {
586 $key = 'wpvulnerability_' . $component;
587
588 if ( is_multisite() ) {
589 delete_site_transient( $key );
590 delete_site_option( 'wpvulnerability-' . $component . '-cache' );
591 } else {
592 delete_transient( $key );
593 delete_option( 'wpvulnerability-' . $component . '-cache' );
594 }
595 }
596
597 return true;
598 }
599
600 /**
601 * Resets plugin signatures (MD5 hashes) for plugins and themes.
602 *
603 * @since 4.3.0
604 *
605 * @return bool True if successful, false otherwise.
606 */
607 function wpvulnerability_debug_reset_signatures() {
608 if ( is_multisite() ) {
609 delete_site_option( 'wpvulnerability-plugins-signature' );
610 delete_site_option( 'wpvulnerability-themes-signature' );
611 } else {
612 delete_option( 'wpvulnerability-plugins-signature' );
613 delete_option( 'wpvulnerability-themes-signature' );
614 }
615
616 return true;
617 }
618
619 /**
620 * Retrieves all database options related to WPVulnerability.
621 *
622 * @since 4.3.0
623 *
624 * @return array<int, string> Array of option names.
625 */
626 function wpvulnerability_debug_get_option_names() {
627 $options = array(
628 'wpvulnerability-config',
629 'wpvulnerability-analyze',
630 'wpvulnerability-statistics',
631 'wpvulnerability-logs',
632 );
633
634 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
635
636 foreach ( $components as $component ) {
637 $options[] = 'wpvulnerability-' . $component;
638 $options[] = 'wpvulnerability-' . $component . '-cache';
639 $options[] = 'wpvulnerability-' . $component . '-version';
640 $options[] = 'wpvulnerability-' . $component . '-vulnerable';
641 }
642
643 $options[] = 'wpvulnerability-plugins-signature';
644 $options[] = 'wpvulnerability-themes-signature';
645
646 return $options;
647 }
648
649 /**
650 * Retrieves the value of a specific WPVulnerability option.
651 *
652 * @since 4.3.0
653 *
654 * @param string $option_name The option name to retrieve.
655 *
656 * @return mixed|null The option value or null if not found.
657 */
658 function wpvulnerability_debug_get_option_value( $option_name ) {
659 if ( is_multisite() ) {
660 return get_site_option( $option_name, null );
661 } else {
662 return get_option( $option_name, null );
663 }
664 }
665