PluginProbe
WPVulnerability / 5.1.6
WPVulnerability v5.1.6
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.6, at wpvulnerability-debug.php

1,084 lines 35.0 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 * Returns the slug of the first installed plugin or theme.
382 *
383 * Mirrors the slug derivation used by the vulnerability data loaders: the
384 * folder name, falling back to the text domain for single-file plugins.
385 *
386 * @since 5.1.6
387 *
388 * @param string $type Either 'plugin' or 'theme'.
389 *
390 * @return string Slug, or an empty string when nothing of that type is installed.
391 */
392 function wpvulnerability_debug_first_installed_slug( $type ) {
393
394 if ( 'theme' === $type ) {
395 foreach ( wp_get_themes() as $theme ) {
396 return $theme->get_stylesheet();
397 }
398 return '';
399 }
400
401 if ( ! function_exists( 'get_plugins' ) ) {
402 require_once ABSPATH . 'wp-admin/includes/plugin.php';
403 }
404 if ( ! function_exists( 'get_plugins' ) ) {
405 return '';
406 }
407
408 foreach ( get_plugins() as $plugin_file => $plugin_data ) {
409 $folder = explode( '/', $plugin_file );
410 $slug = trim( (string) $folder[0] );
411 if ( '' !== $slug && false === strpos( $plugin_file, '/' ) ) {
412 // Single-file plugin at the plugins root: use the file name.
413 $slug = basename( $slug, '.php' );
414 }
415 if ( '' === $slug && isset( $plugin_data['TextDomain'] ) ) {
416 $td_raw = $plugin_data['TextDomain'];
417 $slug = trim( is_scalar( $td_raw ) ? (string) $td_raw : '' );
418 }
419 if ( '' !== $slug ) {
420 return $slug;
421 }
422 }
423
424 return '';
425 }
426
427 /**
428 * Tests API connectivity for a specific component.
429 *
430 * @since 4.3.0
431 * @since 5.1.6 Core collapses pre-release versions to their stable milestone,
432 * and plugins/themes use the real slug-based API routes.
433 *
434 * @param string $component The component to test (e.g., 'core', 'plugins', 'php').
435 *
436 * @return array<string, mixed> Result array with success status, response data, and timing.
437 */
438 function wpvulnerability_debug_test_api_component( $component ) {
439 $result = array(
440 'success' => false,
441 'component' => $component,
442 'http_code' => 0,
443 'response_time' => 0,
444 'message' => '',
445 'data_preview' => '',
446 );
447
448 // Validate component.
449 $valid_components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
450 if ( ! in_array( $component, $valid_components, true ) ) {
451 $result['message'] = __( 'Invalid component specified.', 'wpvulnerability' );
452 return $result;
453 }
454
455 // Build the API URL for the component. Core and software routes take a
456 // version; the plugin/theme routes take a slug only.
457 $url = '';
458 $version = null;
459 switch ( $component ) {
460 case 'core':
461 global $wp_version;
462 // The API only serves stable milestones: collapse pre-release
463 // suffixes (e.g. 7.1-alpha-62421) so the check keeps working.
464 $version = preg_replace( '/-(?:alpha|beta|rc).*$/i', '', trim( (string) $wp_version ) );
465 if ( ! is_string( $version ) || '' === $version ) {
466 $result['message'] = __( 'WordPress version not detected for this component.', 'wpvulnerability' );
467 return $result;
468 }
469 $url = WPVULNERABILITY_API_HOST . 'core/' . wpvulnerability_sanitize_version( $version ) . '/';
470 break;
471 case 'plugins':
472 case 'themes':
473 $slug = wpvulnerability_debug_first_installed_slug( 'plugins' === $component ? 'plugin' : 'theme' );
474 if ( '' === $slug ) {
475 $result['message'] = 'plugins' === $component
476 ? __( 'No plugins detected to test the API endpoint.', 'wpvulnerability' )
477 : __( 'No themes detected to test the API endpoint.', 'wpvulnerability' );
478 return $result;
479 }
480 $url = WPVULNERABILITY_API_HOST . ( 'plugins' === $component ? 'plugin/' : 'theme/' ) . sanitize_title( $slug ) . '/';
481 break;
482 default:
483 if ( function_exists( 'wpvulnerability_get_software_version' ) ) {
484 $version = wpvulnerability_get_software_version( $component );
485 }
486 if ( ! $version ) {
487 $result['message'] = __( 'No local version detected; the API endpoint was not called.', 'wpvulnerability' );
488 return $result;
489 }
490 $url = WPVULNERABILITY_API_HOST . $component . '/' . wpvulnerability_sanitize_version( $version ) . '/';
491 break;
492 }
493
494 // Execute request with timing.
495 $start_time = microtime( true );
496 $response = wp_remote_get(
497 $url,
498 array(
499 'timeout' => 10,
500 )
501 );
502 $end_time = microtime( true );
503
504 $result['response_time'] = round( ( $end_time - $start_time ) * 1000, 2 );
505
506 // Process response.
507 if ( is_wp_error( $response ) ) {
508 $result['message'] = $response->get_error_message();
509 return $result;
510 }
511
512 $result['http_code'] = wp_remote_retrieve_response_code( $response );
513 $body = wp_remote_retrieve_body( $response );
514
515 if ( 200 === $result['http_code'] ) {
516 $result['success'] = true;
517 $result['message'] = __( 'API request successful.', 'wpvulnerability' );
518
519 // Create a preview of the response data.
520 $decoded = json_decode( $body, true );
521 if ( $decoded ) {
522 $encoded = wp_json_encode( $decoded, JSON_PRETTY_PRINT );
523 $preview = false !== $encoded ? $encoded : '';
524 if ( strlen( $preview ) > 500 ) {
525 $preview = substr( $preview, 0, 500 ) . '...';
526 }
527 $result['data_preview'] = $preview;
528 } else {
529 $result['data_preview'] = substr( $body, 0, 500 );
530 }
531 } else {
532 $result['message'] = sprintf(
533 /* translators: %d: HTTP status code */
534 __( 'API returned HTTP code %d.', 'wpvulnerability' ),
535 $result['http_code']
536 );
537 }
538
539 return $result;
540 }
541
542 /**
543 * Retrieves cron job status information.
544 *
545 * @since 4.3.0
546 *
547 * @return array<string, array<string, mixed>> Cron status information.
548 */
549 function wpvulnerability_debug_get_cron_status() {
550 $cron_status = array(
551 'update_database' => array(
552 'hook' => 'wpvulnerability_update_database',
553 'next_run' => null,
554 'last_run' => null,
555 'scheduled' => false,
556 ),
557 'send_notification' => array(
558 'hook' => 'wpvulnerability_notification',
559 'next_run' => null,
560 'last_run' => null,
561 'scheduled' => false,
562 ),
563 );
564
565 // Check update database cron.
566 $next_update = wp_next_scheduled( 'wpvulnerability_update_database' );
567 if ( $next_update ) {
568 $cron_status['update_database']['next_run'] = $next_update;
569 $cron_status['update_database']['scheduled'] = true;
570 }
571
572 // Check notification cron.
573 $next_notification = wp_next_scheduled( 'wpvulnerability_notification' );
574 if ( $next_notification ) {
575 $cron_status['send_notification']['next_run'] = $next_notification;
576 $cron_status['send_notification']['scheduled'] = true;
577 }
578
579 // Last run = most recent API response log (stored as the wpvulnerability_log CPT).
580 $last_log = get_posts(
581 array(
582 'post_type' => 'wpvulnerability_log',
583 'post_status' => 'any',
584 'posts_per_page' => 1,
585 'orderby' => 'date',
586 'order' => 'DESC',
587 'fields' => 'ids',
588 'no_found_rows' => true,
589 )
590 );
591 if ( ! empty( $last_log ) ) {
592 $timestamp = get_post_timestamp( $last_log[0] );
593 if ( false !== $timestamp ) {
594 $cron_status['update_database']['last_run'] = $timestamp;
595 }
596 }
597
598 return $cron_status;
599 }
600
601 /**
602 * Masks sensitive configuration values (webhook URLs, tokens, emails).
603 *
604 * Used whenever configuration data is displayed or exported for debugging,
605 * so shared debug files never contain usable secrets or recipient addresses.
606 *
607 * @since 5.1.3
608 *
609 * @param mixed $config Plugin configuration.
610 * @return array<mixed, mixed> Configuration with sensitive values masked.
611 */
612 function wpvulnerability_debug_mask_config( $config ) {
613 if ( ! is_array( $config ) ) {
614 return array();
615 }
616
617 $secret_keys = array( 'slack_webhook', 'teams_webhook', 'discord_webhook', 'telegram_bot_token', 'telegram_chat_id' );
618
619 foreach ( $secret_keys as $key ) {
620 if ( isset( $config[ $key ] ) && is_scalar( $config[ $key ] ) && '' !== (string) $config[ $key ] ) {
621 $value = (string) $config[ $key ];
622 $config[ $key ] = strlen( $value ) > 8 ? substr( $value, 0, 4 ) . '...' . substr( $value, -4 ) : '...';
623 }
624 }
625
626 if ( isset( $config['emails'] ) && is_scalar( $config['emails'] ) && '' !== (string) $config['emails'] ) {
627 $masked = array();
628 foreach ( explode( ',', (string) $config['emails'] ) as $email ) {
629 $parts = explode( '@', trim( $email ) );
630 if ( count( $parts ) < 2 || '' === $parts[1] ) {
631 $masked[] = '...';
632 continue;
633 }
634 $masked[] = ( '' !== $parts[0] ? substr( $parts[0], 0, 1 ) . '***' : '***' ) . '@' . $parts[1];
635 }
636 $config['emails'] = implode( ',', $masked );
637 }
638
639 return $config;
640 }
641
642 /**
643 * Exports comprehensive debug information as JSON.
644 *
645 * @since 4.3.0
646 *
647 * @return string JSON-encoded debug information.
648 */
649 function wpvulnerability_debug_export_info() {
650 $config = is_multisite()
651 ? get_site_option( 'wpvulnerability-config', array() )
652 : get_option( 'wpvulnerability-config', array() );
653
654 $debug_data = array(
655 'timestamp' => current_time( 'mysql' ),
656 'system_info' => wpvulnerability_debug_get_system_info(),
657 'components' => wpvulnerability_debug_get_component_status(),
658 'configuration' => wpvulnerability_debug_mask_config( $config ),
659 'cron_status' => wpvulnerability_debug_get_cron_status(),
660 'vulnerability_counts' => array(),
661 );
662
663 // Add vulnerability counts for each component.
664 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
665 foreach ( $components as $component ) {
666 $vulnerable_option = is_multisite()
667 ? get_site_option( 'wpvulnerability-' . $component . '-vulnerable', 0 )
668 : get_option( 'wpvulnerability-' . $component . '-vulnerable', 0 );
669 $count = is_numeric( $vulnerable_option ) ? (int) $vulnerable_option : 0;
670 $debug_data['vulnerability_counts'][ $component ] = $count;
671 }
672
673 $encoded_debug = wp_json_encode( $debug_data, JSON_PRETTY_PRINT );
674 return false !== $encoded_debug ? $encoded_debug : '';
675 }
676
677 /**
678 * Clears all plugin caches and transients.
679 *
680 * @since 4.3.0
681 *
682 * @return bool True if successful, false otherwise.
683 */
684 function wpvulnerability_debug_clear_all_caches() {
685 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
686
687 foreach ( $components as $component ) {
688 $key = 'wpvulnerability_' . $component;
689
690 if ( is_multisite() ) {
691 delete_site_transient( $key );
692 delete_site_option( 'wpvulnerability-' . $component . '-cache' );
693 } else {
694 delete_transient( $key );
695 delete_option( 'wpvulnerability-' . $component . '-cache' );
696 }
697 }
698
699 // Plugin data timestamp option, written by wpvulnerability_plugin_get_data().
700 if ( is_multisite() ) {
701 delete_site_option( 'wpvulnerability-plugins-cache-data' );
702 } else {
703 delete_option( 'wpvulnerability-plugins-cache-data' );
704 }
705
706 return true;
707 }
708
709 /**
710 * Resets plugin signatures (MD5 hashes) for plugins and themes.
711 *
712 * @since 4.3.0
713 *
714 * @return bool True if successful, false otherwise.
715 */
716 function wpvulnerability_debug_reset_signatures() {
717 if ( is_multisite() ) {
718 delete_site_option( 'wpvulnerability-plugins-signature' );
719 delete_site_option( 'wpvulnerability-themes-signature' );
720 } else {
721 delete_option( 'wpvulnerability-plugins-signature' );
722 delete_option( 'wpvulnerability-themes-signature' );
723 }
724
725 return true;
726 }
727
728 /**
729 * Retrieves all database options related to WPVulnerability.
730 *
731 * @since 4.3.0
732 *
733 * @return array<int, string> Array of option names.
734 */
735 function wpvulnerability_debug_get_option_names() {
736 $options = array(
737 'wpvulnerability-config',
738 'wpvulnerability-analyze',
739 'wpvulnerability-statistics',
740 'wpvulnerability-logs',
741 );
742
743 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
744
745 foreach ( $components as $component ) {
746 $options[] = 'wpvulnerability-' . $component;
747 $options[] = 'wpvulnerability-' . $component . '-cache';
748 $options[] = 'wpvulnerability-' . $component . '-version';
749 $options[] = 'wpvulnerability-' . $component . '-vulnerable';
750 }
751
752 $options[] = 'wpvulnerability-plugins-signature';
753 $options[] = 'wpvulnerability-themes-signature';
754
755 return $options;
756 }
757
758 /**
759 * Retrieves the value of a specific WPVulnerability option.
760 *
761 * @since 4.3.0
762 *
763 * @param string $option_name The option name to retrieve.
764 *
765 * @return mixed|null The option value or null if not found.
766 */
767 function wpvulnerability_debug_get_option_value( $option_name ) {
768 if ( is_multisite() ) {
769 $value = get_site_option( $option_name, null );
770 } else {
771 $value = get_option( $option_name, null );
772 }
773
774 // Mask secrets when displaying the configuration in the options viewer.
775 if ( 'wpvulnerability-config' === $option_name && is_array( $value ) ) {
776 $value = wpvulnerability_debug_mask_config( $value );
777 }
778
779 return $value;
780 }
781
782 /**
783 * Returns the PHP extensions WordPress makes use of, grouped by relevance.
784 *
785 * Purely informational: presence or absence of an extension is neither good
786 * nor bad. Grouping and notes follow the WordPress core recommendations.
787 *
788 * @since 5.1.6
789 *
790 * @return array<array{label: string, extensions: array<string, string>}> Each group has a
791 * label and a map of extension name to description.
792 */
793 function wpvulnerability_debug_get_php_extensions() {
794 return array(
795 array(
796 'label' => __( 'Built-in (no hosting action required)', 'wpvulnerability' ),
797 'extensions' => array(
798 'pcre' => __( 'Regular expression engine used throughout PHP and WordPress.', 'wpvulnerability' ),
799 ),
800 ),
801 array(
802 'label' => __( 'Required', 'wpvulnerability' ),
803 'extensions' => array(
804 'hash' => __( 'Hashing, including passwords and update packages.', 'wpvulnerability' ),
805 'json' => __( 'Communications with other servers and JSON data processing.', 'wpvulnerability' ),
806 'mysqli' => __( 'Connects to MySQL/MariaDB for database interactions.', 'wpvulnerability' ),
807 ),
808 ),
809 array(
810 'label' => __( 'Highly recommended', 'wpvulnerability' ),
811 'extensions' => array(
812 'curl' => __( 'Performs remote request operations.', 'wpvulnerability' ),
813 'dom' => __( 'Validates Text Widget content and configures IIS7+ automatically.', 'wpvulnerability' ),
814 'exif' => __( 'Works with metadata stored in images.', 'wpvulnerability' ),
815 'fileinfo' => __( 'Detects MIME types of file uploads.', 'wpvulnerability' ),
816 'igbinary' => __( 'Drop-in replacement for the standard PHP serializer; improves performance.', 'wpvulnerability' ),
817 'imagick' => __( 'Better image quality for media uploads and PDF thumbnail support.', 'wpvulnerability' ),
818 'intl' => __( 'Locale-aware operations: formatting, transliteration, collation, timezones.', 'wpvulnerability' ),
819 'mbstring' => __( 'Properly handles UTF-8 text.', 'wpvulnerability' ),
820 'openssl' => __( 'SSL-based connections to other hosts.', 'wpvulnerability' ),
821 'xml' => __( 'XML parsing, such as from a third-party site.', 'wpvulnerability' ),
822 'zip' => __( 'Decompresses Plugin, Theme, and WordPress update packages.', 'wpvulnerability' ),
823 ),
824 ),
825 array(
826 'label' => __( 'Object cache (only one is needed)', 'wpvulnerability' ),
827 'extensions' => array(
828 'apcu' => __( 'In-memory key-value store for PHP.', 'wpvulnerability' ),
829 'memcached' => __( 'Distributed memory object caching system.', 'wpvulnerability' ),
830 'opcache' => __( 'Stores precompiled PHP bytecode to reduce repeated parsing and compilation.', 'wpvulnerability' ),
831 'redis' => __( 'PHP extension for interfacing with Redis.', 'wpvulnerability' ),
832 ),
833 ),
834 array(
835 'label' => __( 'Optional improvements', 'wpvulnerability' ),
836 'extensions' => array(
837 'timezonedb' => __( 'Timezone database used by the PHP date and time functions.', 'wpvulnerability' ),
838 ),
839 ),
840 array(
841 'label' => __( 'Fallbacks / situational use', 'wpvulnerability' ),
842 'extensions' => array(
843 'bcmath' => __( 'Arbitrary precision mathematics.', 'wpvulnerability' ),
844 'filter' => __( 'Securely filters user input.', 'wpvulnerability' ),
845 'gd' => __( 'Functionally limited fallback for image manipulation when Imagick is unavailable.', 'wpvulnerability' ),
846 'iconv' => __( 'Converts between character sets.', 'wpvulnerability' ),
847 'shmop' => __( 'Reads, writes, creates and deletes Unix shared memory segments.', 'wpvulnerability' ),
848 'simplexml' => __( 'XML parsing.', 'wpvulnerability' ),
849 'sodium' => __( 'Signature validation and securely random bytes.', 'wpvulnerability' ),
850 'xmlreader' => __( 'XML parsing.', 'wpvulnerability' ),
851 'zlib' => __( 'Gzip compression and decompression.', 'wpvulnerability' ),
852 ),
853 ),
854 array(
855 'label' => __( 'File changes when files are not writeable (transports)', 'wpvulnerability' ),
856 'extensions' => array(
857 'ssh2' => __( 'Shell, remote execution, tunneling and file transfer over SSH.', 'wpvulnerability' ),
858 'ftp' => __( 'FTP client access for updates and installations.', 'wpvulnerability' ),
859 'sockets' => __( 'Low-level socket communication functions.', 'wpvulnerability' ),
860 ),
861 ),
862 );
863 }
864
865 /**
866 * Detects whether a PHP extension is loaded.
867 *
868 * Handles special cases where the internal extension name differs from the
869 * common name or where availability depends on functions instead.
870 *
871 * @since 5.1.6
872 *
873 * @param string $extension Extension name as listed in the recommendations.
874 *
875 * @return bool True when the extension is loaded.
876 */
877 function wpvulnerability_debug_extension_loaded( $extension ) {
878 if ( 'opcache' === $extension ) {
879 return extension_loaded( 'opcache' )
880 || extension_loaded( 'Zend OPcache' )
881 || function_exists( 'opcache_get_status' );
882 }
883
884 return extension_loaded( $extension );
885 }
886
887 /**
888 * Gathers system package information relevant to WordPress.
889 *
890 * Informational only. Shell probes honour the plugin shell-exec policy
891 * (security mode, disable constant) through the safe wrapper; when probing
892 * is not possible the version is reported as unknown.
893 *
894 * @since 5.1.6
895 *
896 * @return array<array{name: string, available: bool, version: string|null}> Package rows.
897 */
898 function wpvulnerability_debug_get_system_packages() {
899 $packages = array();
900
901 // cURL: reuse the shared detector (PHP extension first, then CLI probe).
902 $curl_version = function_exists( 'wpvulnerability_detect_curl' ) ? wpvulnerability_detect_curl() : null;
903 $packages[] = array(
904 'name' => 'curl',
905 'available' => null !== $curl_version,
906 'version' => $curl_version,
907 );
908
909 // ImageMagick: reuse the shared detector.
910 $imagemagick = function_exists( 'wpvulnerability_detect_imagemagick' ) ? wpvulnerability_detect_imagemagick() : array();
911 $im_raw = isset( $imagemagick['version'] ) ? $imagemagick['version'] : null;
912 $im_version = is_string( $im_raw ) ? $im_raw : null;
913 $packages[] = array(
914 'name' => 'ImageMagick',
915 'available' => null !== $im_version,
916 'version' => $im_version,
917 );
918
919 // Ghost Script: enables Imagick/ImageMagick PDF thumbnail generation.
920 $gs_version = null;
921 if ( wpvulnerability_can_shell_exec() ) {
922 $gs_output = wpvulnerability_safe_shell_exec( 'gs', 'gs --version' );
923 $gs_matches = array();
924 if ( is_string( $gs_output ) && preg_match( '/(\d+\.\d+(?:\.\d+)?)/', trim( $gs_output ), $gs_matches ) ) {
925 $gs_version = $gs_matches[1];
926 }
927 }
928 $packages[] = array(
929 'name' => 'Ghost Script',
930 'available' => null !== $gs_version,
931 'version' => $gs_version,
932 );
933
934 // OpenSSL: report the OpenSSL linked into the PHP build.
935 $openssl_version = null;
936 if ( defined( 'OPENSSL_VERSION_TEXT' ) && preg_match( '/(?:OpenSSL|LibreSSL|BoringSSL)\s+(\d[\w.]*)/', (string) OPENSSL_VERSION_TEXT, $o_matches ) ) {
937 $openssl_version = $o_matches[1];
938 }
939 $packages[] = array(
940 'name' => 'OpenSSL',
941 'available' => defined( 'OPENSSL_VERSION_TEXT' ),
942 'version' => $openssl_version,
943 );
944
945 // WebP and AVIF support: check Imagick formats, GD, then CLI tools.
946 foreach (
947 array(
948 'WebP' => array(
949 'format' => 'WEBP',
950 'gd_key' => 'WebP Support',
951 'tool' => 'cwebp',
952 'command' => 'cwebp -version',
953 ),
954 'AVIF' => array(
955 'format' => 'AVIF',
956 'gd_key' => 'AVIF Support',
957 'tool' => 'avifenc',
958 'command' => 'avifenc --version',
959 ),
960 )
961 as $format_name => $format_check
962 ) {
963 $supported = false;
964 $version = null;
965
966 if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
967 try {
968 $imagick = new Imagick();
969 $supported = in_array( strtoupper( $format_check['format'] ), $imagick->queryFormats( $format_check['format'] ), true );
970 } catch ( Exception $e ) {
971 $supported = false;
972 }
973 }
974
975 if ( ! $supported && extension_loaded( 'gd' ) && function_exists( 'gd_info' ) ) {
976 $gd_info = gd_info();
977 $supported = ! empty( $gd_info[ $format_check['gd_key'] ] );
978 }
979
980 if ( ! $supported && wpvulnerability_can_shell_exec() ) {
981 $tool_output = wpvulnerability_safe_shell_exec( $format_check['tool'], $format_check['command'] );
982 if ( is_string( $tool_output ) && '' !== trim( $tool_output ) ) {
983 $supported = true;
984 $v_matches = array();
985 if ( preg_match( '/(\d+\.\d+(?:\.\d+)?)/', $tool_output, $v_matches ) ) {
986 $version = $v_matches[1];
987 }
988 }
989 }
990
991 $packages[] = array(
992 'name' => $format_name,
993 'available' => $supported,
994 'version' => $version,
995 );
996 }
997
998 return $packages;
999 }
1000
1001 /**
1002 * Renders the Debug tab section: PHP extensions and system packages.
1003 *
1004 * Informational listing only: nothing here is good or bad.
1005 *
1006 * @since 5.1.6
1007 *
1008 * @return void
1009 */
1010 function wpvulnerability_render_debug_section_php_extensions() {
1011 $groups = wpvulnerability_debug_get_php_extensions();
1012 $packages = wpvulnerability_debug_get_system_packages();
1013 ?>
1014 <div class="wpvulnerability-debug-section">
1015 <h3><?php esc_html_e( 'PHP Extensions', 'wpvulnerability' ); ?></h3>
1016 <p>
1017 <?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' ); ?>
1018 </p>
1019 <?php
1020 foreach ( $groups as $group ) :
1021 ?>
1022 <h4><?php echo esc_html( $group['label'] ); ?></h4>
1023 <table class="widefat striped" style="max-width: 720px;">
1024 <tbody>
1025 <?php
1026 foreach ( $group['extensions'] as $extension => $description ) :
1027 $loaded = wpvulnerability_debug_extension_loaded( $extension );
1028 ?>
1029 <tr>
1030 <td style="width: 90px;">
1031 <span style="color: <?php echo $loaded ? '#00a32a;' : '#8c8f94;'; ?>"><?php echo $loaded ? esc_html__( 'Loaded', 'wpvulnerability' ) : esc_html__( 'Not loaded', 'wpvulnerability' ); ?></span>
1032 </td>
1033 <td style="width: 110px;"><code><?php echo esc_html( $extension ); ?></code></td>
1034 <td><?php echo esc_html( $description ); ?></td>
1035 </tr>
1036 <?php
1037 endforeach;
1038 ?>
1039 </tbody>
1040 </table>
1041 <?php
1042 endforeach;
1043 ?>
1044 <h3><?php esc_html_e( 'System Packages', 'wpvulnerability' ); ?></h3>
1045 <p>
1046 <?php esc_html_e( 'System software WordPress can leverage. Informational only.', 'wpvulnerability' ); ?>
1047 </p>
1048 <table class="widefat striped" style="max-width: 720px;">
1049 <tbody>
1050 <?php
1051 foreach ( $packages as $package ) :
1052 ?>
1053 <tr>
1054 <td style="width: 90px;">
1055 <span style="color: <?php echo $package['available'] ? '#00a32a;' : '#8c8f94;'; ?>">
1056 <?php echo $package['available'] ? esc_html__( 'Available', 'wpvulnerability' ) : esc_html__( 'Not detected', 'wpvulnerability' ); ?>
1057 </span>
1058 </td>
1059 <td style="width: 110px;"><code><?php echo esc_html( (string) $package['name'] ); ?></code></td>
1060 <td>
1061 <?php
1062 if ( null !== $package['version'] && '' !== (string) $package['version'] ) {
1063 echo esc_html(
1064 sprintf(
1065 /* translators: %s: package version number */
1066 __( 'Version %s', 'wpvulnerability' ),
1067 (string) $package['version']
1068 )
1069 );
1070 } elseif ( ! $package['available'] ) {
1071 esc_html_e( 'No local version detected.', 'wpvulnerability' );
1072 }
1073 ?>
1074 </td>
1075 </tr>
1076 <?php
1077 endforeach;
1078 ?>
1079 </tbody>
1080 </table>
1081 </div>
1082 <?php
1083 }
1084