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-adminms.php

wpvulnerability-adminms.php in WPVulnerability 5.1.2, at wpvulnerability-adminms.php

4,191 lines 167.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Process functions
4 *
5 * @package WPVulnerability
6 *
7 * @since 2.0.0
8 */
9
10 defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
11
12 /**
13 * Load the settings to be available always.
14 *
15 * @since 2.0.0
16 *
17 * @return array|false An array containing the WPVulnerability settings if they exist, or false if they don't.
18 */
19 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config' );
20 $wpvulnerability_analyze = get_site_option( 'wpvulnerability-analyze' );
21
22 /**
23 * Enqueues the WPVulnerability admin CSS file on WPVulnerability admin pages.
24 *
25 * @since 2.0.0
26 *
27 * @return void
28 */
29 function wpvulnerability_admin_enqueue_scripts() {
30 // Enqueue the admin stylesheet.
31 wp_enqueue_style(
32 'wpvulnerability-admin',
33 WPVULNERABILITY_PLUGIN_URL . 'assets/admin.css',
34 array(),
35 WPVULNERABILITY_PLUGIN_VERSION
36 );
37
38 wp_enqueue_script(
39 'wpvulnerability-admin-js',
40 WPVULNERABILITY_PLUGIN_URL . 'assets/admin.js',
41 array( 'jquery' ),
42 WPVULNERABILITY_PLUGIN_VERSION,
43 true
44 );
45
46 // Localize script with AJAX URL for multisite network admin.
47 wp_localize_script(
48 'wpvulnerability-admin-js',
49 'wpvulnerabilityAjax',
50 array(
51 'ajaxurl' => admin_url( 'admin-ajax.php' ),
52 )
53 );
54 }
55 add_action( 'admin_enqueue_scripts', 'wpvulnerability_admin_enqueue_scripts' );
56
57
58 /**
59 * Processes the form submission for the WPVulnerability plugin settings in a multisite network admin context.
60 *
61 * Checks if the current request is a submission from the WPVulnerability settings page.
62 * Verifies the security nonce to prevent CSRF attacks. If the verification fails or if the request
63 * is not from a network admin or the admin dashboard, the function will halt execution and display an error.
64 * Otherwise, it sanitizes and updates the plugin settings, reschedules any relevant wp-cron events,
65 * and registers a settings error to notify the user that the information has been updated.
66 *
67 * @since 3.0.0
68 * @return void
69 */
70 function wpvulnerability_process_network_config_forms() {
71 // Only process config forms that have the wpvulnerability_submit button.
72 // Other action forms (delete logs, reset, etc.) have their own handlers.
73 if ( ! isset( $_POST['wpvulnerability_submit'] ) ) {
74 return;
75 }
76
77 if ( isset( $_GET['page'] ) && 'wpvulnerability-options' === $_GET['page'] ) {
78
79 if ( check_admin_referer( 'wpvulnerability_nonce', 'wpauto_nonce' ) ) {
80
81 if ( ! current_user_can( 'manage_network_options' ) ) {
82 return;
83 }
84
85 if ( isset( $_POST['wpvulnerability-config'] ) ) {
86
87 $post_config = filter_input( INPUT_POST, 'wpvulnerability-config', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
88 if ( ! is_array( $post_config ) ) {
89 $post_config = array();
90 }
91
92 $wpvulnerability_sanitized_values = wpvulnerability_sanitize_config( $post_config );
93
94 update_site_option( 'wpvulnerability-config', $wpvulnerability_sanitized_values );
95
96 unset( $wpvulnerability_sanitized_values );
97
98 add_settings_error(
99 'wpvulnerability-messages',
100 'wpvulnerability-updated',
101 __( 'Settings saved.', 'wpvulnerability' ),
102 'success'
103 );
104
105 }
106
107 if ( isset( $_POST['wpvulnerability-analyze'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
108
109 $wpvulnerability_sanitized_values = array(
110 'core' => 0,
111 'plugins' => 0,
112 'themes' => 0,
113 'php' => 0,
114 'apache' => 0,
115 'nginx' => 0,
116 'mariadb' => 0,
117 'mysql' => 0,
118 'imagemagick' => 0,
119 'curl' => 0,
120 'memcached' => 0,
121 'redis' => 0,
122 'sqlite' => 0,
123 );
124
125 $wpvulnerability_analyze_raw = (array) wp_unslash( $_POST['wpvulnerability-analyze'] );
126 $wpvulnerability_values = array();
127 foreach ( $wpvulnerability_analyze_raw as $v ) {
128 $wpvulnerability_values[] = sanitize_text_field( is_scalar( $v ) ? (string) $v : '' );
129 }
130
131 foreach ( $wpvulnerability_values as $data ) {
132 switch ( $data ) {
133 case 'core':
134 $wpvulnerability_sanitized_values['core'] = 1;
135 break;
136 case 'plugins':
137 $wpvulnerability_sanitized_values['plugins'] = 1;
138 break;
139 case 'themes':
140 $wpvulnerability_sanitized_values['themes'] = 1;
141 break;
142 case 'php':
143 $wpvulnerability_sanitized_values['php'] = 1;
144 break;
145 case 'apache':
146 $wpvulnerability_sanitized_values['apache'] = 1;
147 break;
148 case 'nginx':
149 $wpvulnerability_sanitized_values['nginx'] = 1;
150 break;
151 case 'mariadb':
152 $wpvulnerability_sanitized_values['mariadb'] = 1;
153 break;
154 case 'mysql':
155 $wpvulnerability_sanitized_values['mysql'] = 1;
156 break;
157 case 'imagemagick':
158 $wpvulnerability_sanitized_values['imagemagick'] = 1;
159 break;
160 case 'curl':
161 $wpvulnerability_sanitized_values['curl'] = 1;
162 break;
163 case 'memcached':
164 $wpvulnerability_sanitized_values['memcached'] = 1;
165 break;
166 case 'redis':
167 $wpvulnerability_sanitized_values['redis'] = 1;
168 break;
169 case 'sqlite':
170 $wpvulnerability_sanitized_values['sqlite'] = 1;
171 break;
172 }
173 }
174
175 update_site_option(
176 'wpvulnerability-analyze',
177 array(
178 'core' => $wpvulnerability_sanitized_values['core'],
179 'plugins' => $wpvulnerability_sanitized_values['plugins'],
180 'themes' => $wpvulnerability_sanitized_values['themes'],
181 'php' => $wpvulnerability_sanitized_values['php'],
182 'apache' => $wpvulnerability_sanitized_values['apache'],
183 'nginx' => $wpvulnerability_sanitized_values['nginx'],
184 'mariadb' => $wpvulnerability_sanitized_values['mariadb'],
185 'mysql' => $wpvulnerability_sanitized_values['mysql'],
186 'imagemagick' => $wpvulnerability_sanitized_values['imagemagick'],
187 'curl' => $wpvulnerability_sanitized_values['curl'],
188 'memcached' => $wpvulnerability_sanitized_values['memcached'],
189 'redis' => $wpvulnerability_sanitized_values['redis'],
190 'sqlite' => $wpvulnerability_sanitized_values['sqlite'],
191 )
192 );
193
194 unset( $wpvulnerability_sanitized_values );
195
196 add_settings_error(
197 'wpvulnerability-messages',
198 'wpvulnerability-updated',
199 __( 'Settings saved.', 'wpvulnerability' ),
200 'success'
201 );
202
203 }
204 }
205 }
206 }
207 add_action( 'admin_init', 'wpvulnerability_process_network_config_forms' );
208
209 /**
210 * Process action form submissions (delete logs, reset, test email, etc.).
211 *
212 * @since 4.3.0
213 * @return void
214 */
215 function wpvulnerability_process_network_action_forms() {
216 /**
217 * Reset the data
218 *
219 * @since 3.0.0
220 */
221 if ( isset( $_POST['wpvulnerability_reset'] ) && check_admin_referer( 'wpvulnerability_reset_action', 'wpvulnerability_reset_nonce' ) ) {
222
223 if ( current_user_can( 'manage_network_options' ) ) {
224 // Calls the reset function.
225 wpvulnerability_update_database_data();
226
227 set_transient( 'wpvulnerability_message_manual_success', __( 'Data from source has been reloaded.', 'wpvulnerability' ), 10 );
228 } else {
229 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to reload data.', 'wpvulnerability' ), 10 );
230 }
231 }
232
233 /**
234 * Send an test email
235 *
236 * @since 3.0.0
237 */
238 if ( isset( $_POST['wpvulnerability_email'] ) && check_admin_referer( 'wpvulnerability_email_action', 'wpvulnerability_email_nonce' ) ) {
239
240 if ( ! function_exists( 'wpvulnerability_execute_notification' ) ) {
241 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-core.php';
242 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-plugins.php';
243 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-themes.php';
244 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-software.php';
245 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-process.php';
246 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-notifications.php';
247 }
248
249 // Calls the notifications function, forced.
250 if ( current_user_can( 'manage_network_options' ) ) {
251 $wpmail = wpvulnerability_execute_notification( true );
252
253 if ( $wpmail ) {
254
255 set_transient( 'wpvulnerability_message_manual_success', __( 'Test email has been sent.', 'wpvulnerability' ), 10 );
256
257 } else {
258
259 set_transient( 'wpvulnerability_message_manual_error', __( 'Test email has failed. Please, check your email settings.', 'wpvulnerability' ), 10 );
260
261 }
262 } else {
263 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to send test emails.', 'wpvulnerability' ), 10 );
264 }
265 }
266
267 /**
268 * Repairs scheduled cron events across the network.
269 *
270 * @since 4.3.0
271 */
272 if ( isset( $_POST['wpvulnerability_repair_cron'] ) && check_admin_referer( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ) ) {
273 if ( current_user_can( 'manage_network_options' ) ) {
274 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
275 $cron_config = is_array( $wpvulnerability_settings ) ? $wpvulnerability_settings : array();
276 wpvulnerability_repair_network_cron_events( $cron_config );
277 set_transient( 'wpvulnerability_message_manual_success', __( 'WPVulnerability cron events have been repaired across the network.', 'wpvulnerability' ), 10 );
278 } else {
279 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to repair cron events.', 'wpvulnerability' ), 10 );
280 }
281 }
282
283 /**
284 * Delete all stored API logs across the network.
285 *
286 * @since 4.3.0
287 */
288 if ( isset( $_POST['wpvulnerability_delete_logs'] ) && check_admin_referer( 'wpvulnerability_delete_logs_action', 'wpvulnerability_delete_logs_nonce' ) ) {
289 if ( current_user_can( 'manage_network_options' ) ) {
290 wpvulnerability_delete_all_logs();
291 set_transient( 'wpvulnerability_message_manual_success', __( 'All logs have been deleted.', 'wpvulnerability' ), 10 );
292 } else {
293 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to delete logs.', 'wpvulnerability' ), 10 );
294 }
295 }
296
297 /**
298 * Fully resets plugin data, settings, and cached API content across the network.
299 *
300 * @since 4.3.0
301 */
302 if ( isset( $_POST['wpvulnerability_delete_on_uninstall'] ) && check_admin_referer( 'wpvulnerability_uninstall_action', 'wpvulnerability_uninstall_nonce' ) ) {
303 if ( current_user_can( 'manage_network_options' ) ) {
304 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
305 if ( ! is_array( $wpvulnerability_settings ) ) {
306 $wpvulnerability_settings = array();
307 }
308 $wpvulnerability_settings['delete_on_uninstall'] = isset( $_POST['delete_on_uninstall'] ) ? 1 : 0;
309 update_site_option( 'wpvulnerability-config', $wpvulnerability_settings );
310 }
311 }
312
313 if ( isset( $_POST['wpvulnerability_full_reset'] ) && check_admin_referer( 'wpvulnerability_full_reset_action', 'wpvulnerability_full_reset_nonce' ) ) {
314 if ( current_user_can( 'manage_network_options' ) ) {
315 wpvulnerability_reset_plugin_data();
316 set_transient( 'wpvulnerability_message_manual_success', __( 'WPVulnerability has been reset to defaults and reloaded.', 'wpvulnerability' ), 10 );
317 } else {
318 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to reset WPVulnerability.', 'wpvulnerability' ), 10 );
319 }
320 }
321
322 /**
323 * Handles debug action: Clear all caches.
324 *
325 * @since 4.3.0
326 */
327 if ( isset( $_POST['wpvulnerability_debug_clear_caches'] ) && check_admin_referer( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ) ) {
328 if ( current_user_can( 'manage_network_options' ) ) {
329 if ( ! function_exists( 'wpvulnerability_debug_clear_all_caches' ) ) {
330 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-debug.php';
331 }
332 wpvulnerability_debug_clear_all_caches();
333 set_transient( 'wpvulnerability_message_manual_success', __( 'All caches have been cleared.', 'wpvulnerability' ), 10 );
334 } else {
335 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to clear caches.', 'wpvulnerability' ), 10 );
336 }
337 }
338
339 /**
340 * Handles debug action: Reset signatures.
341 *
342 * @since 4.3.0
343 */
344 if ( isset( $_POST['wpvulnerability_debug_reset_signatures'] ) && check_admin_referer( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ) ) {
345 if ( current_user_can( 'manage_network_options' ) ) {
346 if ( ! function_exists( 'wpvulnerability_debug_reset_signatures' ) ) {
347 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-debug.php';
348 }
349 wpvulnerability_debug_reset_signatures();
350 set_transient( 'wpvulnerability_message_manual_success', __( 'Plugin and theme signatures have been reset.', 'wpvulnerability' ), 10 );
351 } else {
352 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to reset signatures.', 'wpvulnerability' ), 10 );
353 }
354 }
355
356 /**
357 * Handles debug action: Export debug info.
358 *
359 * @since 4.3.0
360 */
361 if ( isset( $_POST['wpvulnerability_debug_export'] ) && check_admin_referer( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ) ) {
362 if ( current_user_can( 'manage_network_options' ) ) {
363 if ( ! function_exists( 'wpvulnerability_debug_export_info' ) ) {
364 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-debug.php';
365 }
366 $wpvulnerability_debug_info = wpvulnerability_debug_export_info();
367 $wpvulnerability_filename = 'wpvulnerability-debug-' . gmdate( 'Y-m-d-His' ) . '.json';
368
369 header( 'Content-Type: application/json' );
370 header( 'Content-Disposition: attachment; filename="' . $wpvulnerability_filename . '"' );
371 header( 'Content-Length: ' . strlen( $wpvulnerability_debug_info ) );
372 echo $wpvulnerability_debug_info; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
373 exit;
374 } else {
375 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to export debug information.', 'wpvulnerability' ), 10 );
376 }
377 }
378
379 /**
380 * Handles debug action: Run update database now.
381 *
382 * @since 4.3.0
383 */
384 if ( isset( $_POST['wpvulnerability_run_update'] ) && check_admin_referer( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ) ) {
385 if ( current_user_can( 'manage_network_options' ) ) {
386 wpvulnerability_update_database_data();
387 set_transient( 'wpvulnerability_message_manual_success', __( 'Database update has been executed.', 'wpvulnerability' ), 10 );
388 } else {
389 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to run database updates.', 'wpvulnerability' ), 10 );
390 }
391 }
392
393 /**
394 * Handles debug action: Run notification now.
395 *
396 * @since 4.3.0
397 */
398 if ( isset( $_POST['wpvulnerability_run_notification'] ) && check_admin_referer( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ) ) {
399 if ( current_user_can( 'manage_network_options' ) ) {
400 if ( ! function_exists( 'wpvulnerability_execute_notification' ) ) {
401 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-core.php';
402 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-plugins.php';
403 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-themes.php';
404 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-software.php';
405 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-process.php';
406 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-notifications.php';
407 }
408 $wpvulnerability_result = wpvulnerability_execute_notification( true );
409 if ( $wpvulnerability_result ) {
410 set_transient( 'wpvulnerability_message_manual_success', __( 'Notification has been sent.', 'wpvulnerability' ), 10 );
411 } else {
412 set_transient( 'wpvulnerability_message_manual_error', __( 'Notification sending failed.', 'wpvulnerability' ), 10 );
413 }
414 } else {
415 set_transient( 'wpvulnerability_message_manual_error', __( 'You do not have permission to send notifications.', 'wpvulnerability' ), 10 );
416 }
417 }
418 }
419 add_action( 'admin_init', 'wpvulnerability_process_network_action_forms' );
420
421 /**
422 * Create the WP-Admin options page
423 * This function generates the HTML output for the WPVulnerability settings page in the WP-Admin.
424 *
425 * @since 2.0.0
426 *
427 * @return void
428 */
429 function wpvulnerability_create_admin_page() {
430
431 if ( ! current_user_can( 'manage_network_options' ) ) {
432 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpvulnerability' ) );
433 }
434
435 ?>
436 <div class="header-wrap">
437 <div class="wrapper">
438 <div class="header wpvulnerability-header">
439 <div class="logo">
440 <img src="<?php echo esc_url( WPVULNERABILITY_PLUGIN_URL ); ?>assets/icon.svg" style="height: 64px; vertical-align: text-top; width: 64px;" alt="" title="WPVulnerability">
441 <h2><?php esc_html_e( 'WPVulnerability settings', 'wpvulnerability' ); ?></h2>
442 </div>
443 </div>
444 </div>
445 </div>
446 <?php
447 $wpvulnerability_message_manual_success = get_transient( 'wpvulnerability_message_manual_success' );
448 if ( $wpvulnerability_message_manual_success ) {
449 echo '<div class="notice notice-success"><p>' . esc_html( is_scalar( $wpvulnerability_message_manual_success ) ? (string) $wpvulnerability_message_manual_success : '' ) . '</p></div>';
450 delete_transient( 'wpvulnerability_message_manual_success' );
451 unset( $wpvulnerability_message_manual_success );
452 }
453 $wpvulnerability_message_manual_error = get_transient( 'wpvulnerability_message_manual_error' );
454 if ( $wpvulnerability_message_manual_error ) {
455 echo '<div class="notice notice-error"><p>' . esc_html( is_scalar( $wpvulnerability_message_manual_error ) ? (string) $wpvulnerability_message_manual_error : '' ) . '</p></div>';
456 delete_transient( 'wpvulnerability_message_manual_error' );
457 unset( $wpvulnerability_message_manual_error );
458 }
459 ?>
460 <?php settings_errors( 'wpvulnerability-messages', true ); ?>
461
462 <?php
463 $tabs = wpvulnerability_get_network_admin_tabs();
464
465 if ( empty( $tabs ) ) {
466 return;
467 }
468
469 $current_tab = wpvulnerability_get_current_network_admin_tab( $tabs );
470
471 if ( ! isset( $tabs[ $current_tab ] ) ) {
472 $tab_keys = array_keys( $tabs );
473 $current_tab = reset( $tab_keys );
474 }
475
476 ?>
477 <div class="wrap">
478 <div class="wpvulnerability-settings">
479 <h2 class="nav-tab-wrapper wpvulnerability-tab-nav" role="tablist">
480 <?php
481 foreach ( $tabs as $tab_slug => $tab_data ) {
482 $tab_label = isset( $tab_data['label'] ) ? $tab_data['label'] : '';
483 $is_active = ( $tab_slug === $current_tab );
484 $tab_url = add_query_arg(
485 array(
486 'page' => 'wpvulnerability-options',
487 'tab' => $tab_slug,
488 ),
489 network_admin_url( 'settings.php' )
490 );
491 $tab_class = 'nav-tab wpvulnerability-tab-link';
492 if ( $is_active ) {
493 $tab_class .= ' nav-tab-active';
494 }
495 ?>
496 <a
497 href="<?php echo esc_url( $tab_url ); ?>"
498 class="<?php echo esc_attr( $tab_class ); ?>"
499 id="<?php echo esc_attr( 'wpvulnerability-network-tab-link-' . $tab_slug ); ?>"
500 role="tab"
501 aria-controls="<?php echo esc_attr( 'wpvulnerability-network-tab-panel-' . $tab_slug ); ?>"
502 aria-selected="<?php echo $is_active ? 'true' : 'false'; ?>"
503 <?php
504 if ( ! $is_active ) :
505 ?>
506 tabindex="-1"<?php endif; ?>
507 >
508 <?php echo esc_html( $tab_label ); ?>
509 </a>
510 <?php
511 }
512 ?>
513 </h2>
514 <div
515 id="<?php echo esc_attr( 'wpvulnerability-network-tab-panel-' . $current_tab ); ?>"
516 class="wpvulnerability-tab-panel is-active"
517 role="tabpanel"
518 aria-labelledby="<?php echo esc_attr( 'wpvulnerability-network-tab-link-' . $current_tab ); ?>"
519 tabindex="0"
520 >
521 <?php wpvulnerability_render_network_admin_tab( $current_tab ); ?>
522 </div>
523 </div>
524 </div>
525 <?php
526 }
527
528
529 /**
530 * Retrieves the available tabs for the multisite admin settings page.
531 *
532 * @since 4.1.2
533 *
534 * @return array<string, array<string, string>> An associative array of tab slugs and their labels.
535 */
536 function wpvulnerability_get_network_admin_tabs() {
537 $tabs = array(
538 'notifications' => array(
539 'label' => __( 'Notifications', 'wpvulnerability' ),
540 ),
541 'analysis' => array(
542 'label' => __( 'Analysis', 'wpvulnerability' ),
543 ),
544 'logs' => array(
545 'label' => __( 'Logs', 'wpvulnerability' ),
546 ),
547 'security' => array(
548 'label' => __( 'Security', 'wpvulnerability' ),
549 ),
550 'tools' => array(
551 'label' => __( 'Tools', 'wpvulnerability' ),
552 ),
553 );
554
555 // Add Debug tab only if WP_DEBUG is enabled.
556 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
557 $tabs['debug'] = array(
558 'label' => __( 'Debug', 'wpvulnerability' ),
559 );
560 }
561
562 $tabs['about'] = array(
563 'label' => __( 'About', 'wpvulnerability' ),
564 );
565
566 return $tabs;
567 }
568
569 /**
570 * Determines the active multisite admin tab.
571 *
572 * @since 4.1.2
573 *
574 * @param array<string, array<string, string>> $tabs Registered multisite admin tabs.
575 *
576 * @return string The active tab slug.
577 */
578 function wpvulnerability_get_current_network_admin_tab( $tabs ) {
579 $tab_keys = array_keys( $tabs );
580 $default = reset( $tab_keys );
581 $tab_filter = filter_input( INPUT_GET, 'tab', FILTER_SANITIZE_SPECIAL_CHARS );
582
583 if ( $tab_filter ) {
584 $tab_filter = sanitize_key( $tab_filter );
585 }
586
587 if ( $tab_filter && isset( $tabs[ $tab_filter ] ) ) {
588 return $tab_filter;
589 }
590
591 return $default ? $default : 'notifications';
592 }
593
594 /**
595 * Renders the requested multisite admin tab content.
596 *
597 * @since 4.1.2
598 *
599 * @param string $tab Tab slug to render.
600 *
601 * @return void
602 */
603 function wpvulnerability_render_network_admin_tab( $tab ) {
604 switch ( $tab ) {
605 case 'analysis':
606 wpvulnerability_render_network_admin_tab_analysis();
607 break;
608 case 'logs':
609 wpvulnerability_render_network_admin_tab_logs();
610 break;
611 case 'security':
612 wpvulnerability_render_network_admin_tab_security();
613 break;
614 case 'tools':
615 wpvulnerability_render_network_admin_tab_tools();
616 break;
617 case 'debug':
618 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
619 wpvulnerability_render_network_admin_tab_debug();
620 }
621 break;
622 case 'about':
623 wpvulnerability_render_network_admin_tab_about();
624 break;
625 case 'notifications':
626 default:
627 wpvulnerability_render_network_admin_tab_notifications();
628 break;
629 }
630 }
631
632 /**
633 * Outputs the Notifications tab contents for the multisite settings screen.
634 *
635 * @since 4.1.2
636 *
637 * @return void
638 */
639 function wpvulnerability_render_network_admin_tab_notifications() {
640 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
641 if ( ! is_array( $wpvulnerability_settings ) ) {
642 $wpvulnerability_settings = array();
643 }
644 $defaults = array(
645 'cache' => 12,
646 'period' => 'weekly',
647 'day' => 'monday',
648 'hour' => 0,
649 'minute' => 0,
650 'emails' => '',
651 'slack_webhook' => '',
652 'teams_webhook' => '',
653 'discord_webhook' => '',
654 'telegram_bot_token' => '',
655 'telegram_chat_id' => '',
656 'notify' => array(
657 'email' => 'y',
658 'slack' => 'n',
659 'teams' => 'n',
660 'discord' => 'n',
661 'telegram' => 'n',
662 ),
663 );
664 $wpvulnerability_settings = wp_parse_args( $wpvulnerability_settings, $defaults );
665
666 // Normalize notify settings.
667 if ( ! is_array( $wpvulnerability_settings['notify'] ) ) {
668 $wpvulnerability_settings['notify'] = $defaults['notify'];
669 } else {
670 $wpvulnerability_settings['notify'] = wp_parse_args( $wpvulnerability_settings['notify'], $defaults['notify'] );
671 }
672 $wpvulnerability_settings['notify'] = wpvulnerability_normalize_notify_settings( $wpvulnerability_settings['notify'] );
673
674 $email_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['email'] );
675 $slack_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['slack'] );
676 $teams_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['teams'] );
677 $discord_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['discord'] );
678 $telegram_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['telegram'] );
679
680 // Check if cache is forced.
681 $forced_cache = null;
682 $cache_options = array( 1, 6, 12, 24 );
683 if ( defined( 'WPVULNERABILITY_CACHE_HOURS' ) ) {
684 $forced_cache = (int) WPVULNERABILITY_CACHE_HOURS;
685 if ( ! in_array( $forced_cache, $cache_options, true ) ) {
686 $cache_options[] = $forced_cache;
687 sort( $cache_options, SORT_NUMERIC );
688 }
689 }
690 $current_cache = null !== $forced_cache ? $forced_cache : (int) $wpvulnerability_settings['cache'];
691
692 $admin_email = get_site_option( 'admin_email' );
693 ?>
694 <section class="section wpvulnerability-notifications-panel">
695
696 <form method="post" action="">
697 <?php wp_nonce_field( 'wpvulnerability_nonce', 'wpauto_nonce' ); ?>
698
699 <div class="wpvulnerability-security-section">
700 <h3><?php esc_html_e( 'Notification Settings', 'wpvulnerability' ); ?></h3>
701
702 <div class="wpvulnerability-intro">
703 <p><strong><?php esc_html_e( 'Configure how and when you want to receive vulnerability notifications.', 'wpvulnerability' ); ?></strong></p>
704 <p><?php esc_html_e( 'Stay informed about security vulnerabilities in your WordPress installation, plugins, themes, and server software.', 'wpvulnerability' ); ?></p>
705 </div>
706
707 <!-- Cache Settings -->
708 <div class="wpvulnerability-setting-group">
709 <div class="wpvulnerability-setting-label">
710 <span class="wpvulnerability-setting-icon">⏱️</span>
711 <?php esc_html_e( 'Cache Expiration Time', 'wpvulnerability' ); ?>
712 <?php if ( null !== $forced_cache ) : ?>
713 <span class="wpvulnerability-forced-badge"><?php esc_html_e( 'FORCED', 'wpvulnerability' ); ?></span>
714 <?php endif; ?>
715 </div>
716 <div class="wpvulnerability-setting-description">
717 <?php esc_html_e( 'How long to cache vulnerability data before refreshing from the API.', 'wpvulnerability' ); ?>
718 </div>
719 <select name="wpvulnerability-config[cache]" id="wpvulnerability_cache" <?php disabled( null !== $forced_cache ); ?>>
720 <?php foreach ( $cache_options as $hours ) : ?>
721 <option value="<?php echo esc_attr( (string) $hours ); ?>" <?php selected( $current_cache, $hours ); ?>>
722 <?php
723 echo esc_html(
724 sprintf(
725 /* translators: %d: number of hours */
726 _n( '%d hour', '%d hours', $hours, 'wpvulnerability' ),
727 $hours
728 )
729 );
730 ?>
731 </option>
732 <?php endforeach; ?>
733 </select>
734 <?php if ( null !== $forced_cache ) : ?>
735 <input type="hidden" name="wpvulnerability-config[cache]" value="<?php echo esc_attr( (string) $current_cache ); ?>" />
736 <?php endif; ?>
737 <div class="wpvulnerability-info-box">
738 <p>
739 <?php
740 printf(
741 /* translators: %s: documentation URL */
742 wp_kses_post( __( 'You can force the cache time via wp-config.php constant. <a href="%s" target="_blank" rel="noopener noreferrer">Learn more →</a>', 'wpvulnerability' ) ),
743 esc_url( 'https://www.wpvulnerability.com/plugin/#cache-duration' )
744 );
745 ?>
746 </p>
747 </div>
748 </div>
749
750 <!-- Notification Frequency -->
751 <div class="wpvulnerability-setting-group">
752 <div class="wpvulnerability-setting-label">
753 <span class="wpvulnerability-setting-icon">�
754 </span>
755 <?php esc_html_e( 'Notification Frequency', 'wpvulnerability' ); ?>
756 </div>
757 <div class="wpvulnerability-setting-description">
758 <?php esc_html_e( 'Choose how often you want to receive vulnerability notifications.', 'wpvulnerability' ); ?>
759 </div>
760 <div class="wpvulnerability-radio-group">
761 <label>
762 <input type="radio" name="wpvulnerability-config[period]" value="never" <?php checked( $wpvulnerability_settings['period'], 'never' ); ?> onchange="wpvUpdateScheduleVisibility()" />
763 <?php esc_html_e( 'Never - Disable automatic notifications', 'wpvulnerability' ); ?>
764 </label>
765 <label>
766 <input type="radio" name="wpvulnerability-config[period]" value="daily" <?php checked( $wpvulnerability_settings['period'], 'daily' ); ?> onchange="wpvUpdateScheduleVisibility()" />
767 <?php esc_html_e( 'Daily - Receive notifications every day', 'wpvulnerability' ); ?>
768 </label>
769 <label>
770 <input type="radio" name="wpvulnerability-config[period]" value="weekly" <?php checked( $wpvulnerability_settings['period'], 'weekly' ); ?> onchange="wpvUpdateScheduleVisibility()" />
771 <?php esc_html_e( 'Weekly - Receive notifications once a week', 'wpvulnerability' ); ?>
772 </label>
773 </div>
774
775 <div class="wpvulnerability-schedule-controls" id="wpvulnerability-schedule-controls">
776 <div class="wpvulnerability-schedule-row" id="wpvulnerability-day-selector">
777 <label for="wpvulnerability_day"><?php esc_html_e( 'Day:', 'wpvulnerability' ); ?></label>
778 <select name="wpvulnerability-config[day]" id="wpvulnerability_day">
779 <option value="monday" <?php selected( $wpvulnerability_settings['day'], 'monday' ); ?>><?php esc_html_e( 'Monday', 'wpvulnerability' ); ?></option>
780 <option value="tuesday" <?php selected( $wpvulnerability_settings['day'], 'tuesday' ); ?>><?php esc_html_e( 'Tuesday', 'wpvulnerability' ); ?></option>
781 <option value="wednesday" <?php selected( $wpvulnerability_settings['day'], 'wednesday' ); ?>><?php esc_html_e( 'Wednesday', 'wpvulnerability' ); ?></option>
782 <option value="thursday" <?php selected( $wpvulnerability_settings['day'], 'thursday' ); ?>><?php esc_html_e( 'Thursday', 'wpvulnerability' ); ?></option>
783 <option value="friday" <?php selected( $wpvulnerability_settings['day'], 'friday' ); ?>><?php esc_html_e( 'Friday', 'wpvulnerability' ); ?></option>
784 <option value="saturday" <?php selected( $wpvulnerability_settings['day'], 'saturday' ); ?>><?php esc_html_e( 'Saturday', 'wpvulnerability' ); ?></option>
785 <option value="sunday" <?php selected( $wpvulnerability_settings['day'], 'sunday' ); ?>><?php esc_html_e( 'Sunday', 'wpvulnerability' ); ?></option>
786 </select>
787 </div>
788 <div class="wpvulnerability-schedule-row">
789 <label for="wpvulnerability_hour"><?php esc_html_e( 'Time:', 'wpvulnerability' ); ?></label>
790 <input type="number" min="0" max="23" name="wpvulnerability-config[hour]" id="wpvulnerability_hour" value="<?php echo esc_attr( (string) $wpvulnerability_settings['hour'] ); ?>" />
791 <span>:</span>
792 <input type="number" min="0" max="59" name="wpvulnerability-config[minute]" id="wpvulnerability_minute" value="<?php echo esc_attr( (string) $wpvulnerability_settings['minute'] ); ?>" />
793 <span class="wpvulnerability-input-hint"><?php esc_html_e( '(24-hour format, server timezone)', 'wpvulnerability' ); ?></span>
794 </div>
795 </div>
796 </div>
797
798 <!-- Notification Channels -->
799 <div class="wpvulnerability-setting-group">
800 <div class="wpvulnerability-setting-label">
801 <span class="wpvulnerability-setting-icon">📢</span>
802 <?php esc_html_e( 'Notification Channels', 'wpvulnerability' ); ?>
803 </div>
804 <div class="wpvulnerability-setting-description">
805 <?php esc_html_e( 'Select where you want to receive notifications.', 'wpvulnerability' ); ?>
806 </div>
807 <div class="wpvulnerability-checkbox-group">
808 <label>
809 <input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> onchange="wpvToggleChannelInput('email')" />
810 <?php esc_html_e( 'Email', 'wpvulnerability' ); ?>
811 </label>
812 <div class="wpvulnerability-channel-inputs" id="wpvulnerability-email-inputs">
813 <label for="wpvulnerability_emails"><?php esc_html_e( 'Email Addresses (separated by commas):', 'wpvulnerability' ); ?></label>
814 <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[emails]" id="wpvulnerability_emails" placeholder="<?php echo esc_attr( is_scalar( $admin_email ) ? (string) $admin_email : '' ); ?>" value="<?php echo esc_attr( is_scalar( $wpvulnerability_settings['emails'] ) ? (string) $wpvulnerability_settings['emails'] : '' ); ?>" />
815 <span class="wpvulnerability-input-hint"><?php esc_html_e( 'Default:', 'wpvulnerability' ); ?> <?php echo esc_html( is_scalar( $admin_email ) ? (string) $admin_email : '' ); ?></span>
816 </div>
817
818 <label>
819 <input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> onchange="wpvToggleChannelInput('slack')" />
820 <?php esc_html_e( 'Slack', 'wpvulnerability' ); ?>
821 </label>
822 <div class="wpvulnerability-channel-inputs" id="wpvulnerability-slack-inputs">
823 <label for="wpvulnerability_slack_webhook"><?php esc_html_e( 'Slack Webhook URL:', 'wpvulnerability' ); ?></label>
824 <input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="https://hooks.slack.com/services/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['slack_webhook'] ); ?>" />
825 <span class="wpvulnerability-input-hint">
826 <?php
827 printf(
828 /* translators: %s: Slack documentation URL */
829 wp_kses_post( __( '<a href="%s" target="_blank" rel="noopener noreferrer">Learn how to create a Slack incoming webhook →</a>', 'wpvulnerability' ) ),
830 esc_url( 'https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/' )
831 );
832 ?>
833 </span>
834 </div>
835
836 <label>
837 <input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> onchange="wpvToggleChannelInput('teams')" />
838 <?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?>
839 </label>
840 <div class="wpvulnerability-channel-inputs" id="wpvulnerability-teams-inputs">
841 <label for="wpvulnerability_teams_webhook"><?php esc_html_e( 'Teams Webhook URL:', 'wpvulnerability' ); ?></label>
842 <input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="https://outlook.office.com/webhook/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['teams_webhook'] ); ?>" />
843 <span class="wpvulnerability-input-hint">
844 <?php
845 printf(
846 /* translators: %s: Microsoft Teams documentation URL */
847 wp_kses_post( __( '<a href="%s" target="_blank" rel="noopener noreferrer">Learn how to create a Teams incoming webhook →</a>', 'wpvulnerability' ) ),
848 esc_url( 'https://learn.microsoft.com/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook' )
849 );
850 ?>
851 </span>
852 </div>
853
854 <label>
855 <input type="checkbox" name="wpvulnerability-config[notify][discord]" value="y" <?php checked( $discord_enabled ); ?> onchange="wpvToggleChannelInput('discord')" />
856 <?php esc_html_e( 'Discord', 'wpvulnerability' ); ?>
857 </label>
858 <div class="wpvulnerability-channel-inputs" id="wpvulnerability-discord-inputs">
859 <label for="wpvulnerability_discord_webhook"><?php esc_html_e( 'Discord Webhook URL:', 'wpvulnerability' ); ?></label>
860 <input class="wpvulnerability-input-full" type="url" name="wpvulnerability-config[discord_webhook]" id="wpvulnerability_discord_webhook" placeholder="https://discord.com/api/webhooks/..." value="<?php echo esc_attr( (string) $wpvulnerability_settings['discord_webhook'] ); ?>" />
861 <span class="wpvulnerability-input-hint">
862 <?php
863 printf(
864 /* translators: %s: Discord documentation URL */
865 wp_kses_post( __( '<a href="%s" target="_blank" rel="noopener noreferrer">Learn how to create a Discord webhook →</a>', 'wpvulnerability' ) ),
866 esc_url( 'https://support.discord.com/hc/articles/228383668' )
867 );
868 ?>
869 </span>
870 </div>
871
872 <label>
873 <input type="checkbox" name="wpvulnerability-config[notify][telegram]" value="y" <?php checked( $telegram_enabled ); ?> onchange="wpvToggleChannelInput('telegram')" />
874 <?php esc_html_e( 'Telegram', 'wpvulnerability' ); ?>
875 </label>
876 <div class="wpvulnerability-channel-inputs" id="wpvulnerability-telegram-inputs">
877 <label for="wpvulnerability_telegram_bot_token"><?php esc_html_e( 'Telegram Bot Token:', 'wpvulnerability' ); ?></label>
878 <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_bot_token]" id="wpvulnerability_telegram_bot_token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" value="<?php echo esc_attr( (string) $wpvulnerability_settings['telegram_bot_token'] ); ?>" />
879 <span class="wpvulnerability-input-hint"><?php esc_html_e( 'Format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ); ?></span>
880
881 <label for="wpvulnerability_telegram_chat_id" style="margin-top: 12px;"><?php esc_html_e( 'Telegram Chat ID:', 'wpvulnerability' ); ?></label>
882 <input class="wpvulnerability-input-full" type="text" name="wpvulnerability-config[telegram_chat_id]" id="wpvulnerability_telegram_chat_id" placeholder="-1001234567890" value="<?php echo esc_attr( (string) $wpvulnerability_settings['telegram_chat_id'] ); ?>" />
883 <span class="wpvulnerability-input-hint">
884 <?php
885 printf(
886 /* translators: %s: Telegram documentation URL */
887 wp_kses_post( __( '<a href="%s" target="_blank" rel="noopener noreferrer">Learn how to create a Telegram bot and get Chat ID →</a>', 'wpvulnerability' ) ),
888 esc_url( 'https://core.telegram.org/bots' )
889 );
890 ?>
891 </span>
892 </div>
893 </div>
894 </div>
895
896 <div class="wpvulnerability-save-section">
897 <?php submit_button( __( 'Save Notification Settings', 'wpvulnerability' ), 'primary', 'wpvulnerability_submit', false ); ?>
898 </div>
899 </div>
900 </form>
901
902 <script>
903 function wpvUpdateScheduleVisibility() {
904 var period = document.querySelector('input[name="wpvulnerability-config[period]"]:checked').value;
905 var scheduleControls = document.getElementById('wpvulnerability-schedule-controls');
906 var daySelector = document.getElementById('wpvulnerability-day-selector');
907
908 if (period === 'never') {
909 scheduleControls.classList.add('wpvulnerability-hidden');
910 } else {
911 scheduleControls.classList.remove('wpvulnerability-hidden');
912 if (period === 'weekly') {
913 daySelector.style.display = 'block';
914 } else {
915 daySelector.style.display = 'none';
916 }
917 }
918 }
919
920 function wpvToggleChannelInput(channel) {
921 var checkbox = document.querySelector('input[name="wpvulnerability-config[notify][' + channel + ']"]');
922 var inputs = document.getElementById('wpvulnerability-' + channel + '-inputs');
923
924 if (checkbox.checked) {
925 inputs.classList.remove('wpvulnerability-hidden');
926 } else {
927 inputs.classList.add('wpvulnerability-hidden');
928 }
929 }
930
931 // Initialize visibility on page load.
932 document.addEventListener('DOMContentLoaded', function() {
933 wpvUpdateScheduleVisibility();
934 wpvToggleChannelInput('email');
935 wpvToggleChannelInput('slack');
936 wpvToggleChannelInput('teams');
937 wpvToggleChannelInput('discord');
938 wpvToggleChannelInput('telegram');
939 });
940 </script>
941 </section>
942 <?php
943 }
944
945 /**
946 * Outputs the Analysis tab contents for the multisite settings screen.
947 *
948 * @since 4.1.2
949 *
950 * @return void
951 */
952 function wpvulnerability_render_network_admin_tab_analysis() {
953 $wpvulnerability_analyze = get_site_option( 'wpvulnerability-analyze', array() );
954 if ( ! is_array( $wpvulnerability_analyze ) ) {
955 $wpvulnerability_analyze = array();
956 }
957 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
958 $forced = array();
959
960 foreach ( $components as $component ) {
961 if ( ! isset( $wpvulnerability_analyze[ $component ] ) ) {
962 $wpvulnerability_analyze[ $component ] = 0;
963 }
964 $constant = 'WPVULNERABILITY_HIDE_' . strtoupper( (string) $component );
965 $forced[ $component ] = defined( $constant ) && constant( $constant );
966 if ( $forced[ $component ] ) {
967 $wpvulnerability_analyze[ $component ] = 1;
968 }
969 }
970
971 // Component configuration with labels and icons.
972 $component_config = array(
973 'wordpress' => array(
974 'label' => __( 'WordPress Components', 'wpvulnerability' ),
975 'items' => array(
976 'core' => array(
977 'label' => __( 'WordPress Core', 'wpvulnerability' ),
978 'icon' => '🌐',
979 ),
980 'plugins' => array(
981 'label' => __( 'Plugins', 'wpvulnerability' ),
982 'icon' => '🧩',
983 ),
984 'themes' => array(
985 'label' => __( 'Themes', 'wpvulnerability' ),
986 'icon' => '🎨',
987 ),
988 ),
989 ),
990 'software' => array(
991 'label' => __( 'Software & Languages', 'wpvulnerability' ),
992 'items' => array(
993 'php' => array(
994 'label' => __( 'PHP', 'wpvulnerability' ),
995 'icon' => '🐘',
996 ),
997 ),
998 ),
999 'webservers' => array(
1000 'label' => __( 'Web Servers', 'wpvulnerability' ),
1001 'items' => array(
1002 'apache' => array(
1003 'label' => __( 'Apache HTTPD', 'wpvulnerability' ),
1004 'icon' => '🪶',
1005 ),
1006 'nginx' => array(
1007 'label' => __( 'nginx', 'wpvulnerability' ),
1008 'icon' => '🟩',
1009 ),
1010 ),
1011 ),
1012 'databases' => array(
1013 'label' => __( 'Databases', 'wpvulnerability' ),
1014 'items' => array(
1015 'mariadb' => array(
1016 'label' => __( 'MariaDB', 'wpvulnerability' ),
1017 'icon' => '🐬',
1018 ),
1019 'mysql' => array(
1020 'label' => __( 'MySQL', 'wpvulnerability' ),
1021 'icon' => '🐬',
1022 ),
1023 'sqlite' => array(
1024 'label' => __( 'SQLite', 'wpvulnerability' ),
1025 'icon' => '💾',
1026 ),
1027 ),
1028 ),
1029 'tools' => array(
1030 'label' => __( 'Additional Tools', 'wpvulnerability' ),
1031 'items' => array(
1032 'imagemagick' => array(
1033 'label' => __( 'ImageMagick', 'wpvulnerability' ),
1034 'icon' => '🖼️',
1035 ),
1036 'curl' => array(
1037 'label' => __( 'curl', 'wpvulnerability' ),
1038 'icon' => '🌐',
1039 ),
1040 'memcached' => array(
1041 'label' => __( 'memcached', 'wpvulnerability' ),
1042 'icon' => '',
1043 ),
1044 'redis' => array(
1045 'label' => __( 'Redis', 'wpvulnerability' ),
1046 'icon' => '🔴',
1047 ),
1048 ),
1049 ),
1050 );
1051 ?>
1052 <section class="section wpvulnerability-analysis-panel">
1053
1054 <form method="post" action="">
1055 <?php wp_nonce_field( 'wpvulnerability_nonce', 'wpauto_nonce' ); ?>
1056
1057 <div class="wpvulnerability-security-section">
1058 <h3><?php esc_html_e( 'Component Analysis Settings', 'wpvulnerability' ); ?></h3>
1059
1060 <div class="wpvulnerability-intro">
1061 <p><strong><?php esc_html_e( 'Enable or disable vulnerability analysis for specific components.', 'wpvulnerability' ); ?></strong></p>
1062 <p><?php esc_html_e( 'Check the components you want to HIDE from vulnerability scans and reports. Unchecked components will be actively analyzed. Components with active analysis are shown in green, hidden components in red.', 'wpvulnerability' ); ?></p>
1063 </div>
1064
1065 <?php foreach ( $component_config as $category_key => $category_data ) : ?>
1066 <div class="wpvulnerability-category">
1067 <div class="wpvulnerability-category-title"><?php echo esc_html( $category_data['label'] ); ?></div>
1068 <div class="wpvulnerability-components-grid">
1069 <?php foreach ( $category_data['items'] as $component => $component_data ) : ?>
1070 <?php
1071 $is_checked = ! empty( $wpvulnerability_analyze[ $component ] );
1072 $is_forced = ! empty( $forced[ $component ] );
1073 $card_class = 'wpvulnerability-component-card';
1074 if ( $is_checked ) {
1075 $card_class .= ' wpvulnerability-checked';
1076 }
1077 if ( $is_forced ) {
1078 $card_class .= ' wpvulnerability-disabled';
1079 }
1080 ?>
1081 <div class="<?php echo esc_attr( $card_class ); ?>" onclick="if (!this.classList.contains('wpvulnerability-disabled')) { var cb = this.querySelector('input[type=checkbox]'); cb.checked = !cb.checked; this.classList.toggle('wpvulnerability-checked'); }">
1082 <label>
1083 <span class="wpvulnerability-component-icon"><?php echo esc_html( $component_data['icon'] ); ?></span>
1084 <span class="wpvulnerability-component-content">
1085 <span class="wpvulnerability-component-label">
1086 <?php echo esc_html( $component_data['label'] ); ?>
1087 <?php if ( $is_forced ) : ?>
1088 <span class="wpvulnerability-forced-badge"><?php esc_html_e( 'FORCED', 'wpvulnerability' ); ?></span>
1089 <?php endif; ?>
1090 </span>
1091 <span class="wpvulnerability-component-status">
1092 <?php
1093 if ( $is_forced ) {
1094 echo '<span class="wpvulnerability-status-badge wpvulnerability-status-hidden">' . esc_html__( 'Hidden (forced by constant)', 'wpvulnerability' ) . '</span>';
1095 } elseif ( $is_checked ) {
1096 // Checked = value is 1 = analysis is DISABLED (hidden).
1097 echo '<span class="wpvulnerability-status-badge wpvulnerability-status-hidden">' . esc_html__( '🔴 Analysis disabled', 'wpvulnerability' ) . '</span>';
1098 } else {
1099 // Unchecked = value is 0 or not set = analysis is ENABLED (active).
1100 echo '<span class="wpvulnerability-status-badge wpvulnerability-status-active">' . esc_html__( '🟢 Analysis active', 'wpvulnerability' ) . '</span>';
1101 }
1102 ?>
1103 </span>
1104 </span>
1105 <input
1106 type="checkbox"
1107 name="wpvulnerability-analyze[<?php echo esc_attr( $component ); ?>]"
1108 value="<?php echo esc_attr( $component ); ?>"
1109 <?php checked( $is_checked ); ?>
1110 <?php disabled( $is_forced ); ?>
1111 onclick="event.stopPropagation();"
1112 />
1113 </label>
1114 </div>
1115 <?php endforeach; ?>
1116 </div>
1117 </div>
1118 <?php endforeach; ?>
1119
1120 <div class="wpvulnerability-info-box">
1121 <p>
1122 <strong><?php esc_html_e( 'Tip:', 'wpvulnerability' ); ?></strong>
1123 <?php
1124 printf(
1125 /* translators: %s: documentation URL */
1126 wp_kses_post( __( 'You can force-disable components via wp-config.php constants. <a href="%s" target="_blank" rel="noopener noreferrer">Learn more about force-hiding checks →</a>', 'wpvulnerability' ) ),
1127 esc_url( 'https://www.wpvulnerability.com/plugin/#force-hiding-checks' )
1128 );
1129 ?>
1130 </p>
1131 </div>
1132
1133 <div class="wpvulnerability-save-section">
1134 <?php submit_button( __( 'Save Analysis Settings', 'wpvulnerability' ), 'primary', 'wpvulnerability_submit', false ); ?>
1135 </div>
1136 </div>
1137 </form>
1138 </section>
1139 <?php
1140 }
1141
1142 /**
1143 * Outputs the Logs tab contents for the multisite settings screen.
1144 *
1145 * @since 4.2.0
1146 *
1147 * @return void
1148 */
1149 function wpvulnerability_render_network_admin_tab_logs() {
1150 $choices = wpvulnerability_get_log_retention_values();
1151 $current = wpvulnerability_log_retention_days();
1152 $forced = wpvulnerability_forced_log_retention();
1153 $per_page_options = wpvulnerability_get_log_per_page_options();
1154 $logs_per_page = wpvulnerability_get_default_log_per_page();
1155 $per_page_request = isset( $_GET['logs_per_page'] ) ? absint( (int) wp_unslash( is_string( $_GET['logs_per_page'] ) ? $_GET['logs_per_page'] : '' ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1156 if ( in_array( $per_page_request, $per_page_options, true ) ) {
1157 $logs_per_page = $per_page_request;
1158 }
1159 $current_page = isset( $_GET['log_page'] ) ? absint( (int) wp_unslash( is_string( $_GET['log_page'] ) ? $_GET['log_page'] : '' ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1160 if ( $current_page < 1 ) {
1161 $current_page = 1;
1162 }
1163 $total_logs = wpvulnerability_count_api_logs();
1164 $total_pages = max( 1, (int) ceil( $total_logs / $logs_per_page ) );
1165 if ( $current_page > $total_pages ) {
1166 $current_page = $total_pages;
1167 }
1168 $logs = wpvulnerability_get_api_logs( $logs_per_page, $current_page );
1169 $logs_page_url = add_query_arg(
1170 array(
1171 'page' => 'wpvulnerability-options',
1172 'tab' => 'logs',
1173 'logs_per_page' => $logs_per_page,
1174 'log_page' => $current_page,
1175 ),
1176 network_admin_url( 'settings.php' )
1177 );
1178 $requested_log = isset( $_GET['log'] ) ? absint( (int) wp_unslash( is_string( $_GET['log'] ) ? $_GET['log'] : '' ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1179 $view_log = null;
1180 $log_error = '';
1181 $pagination = '';
1182
1183 if ( $total_pages > 1 ) {
1184 $pagination_base = remove_query_arg(
1185 array( 'log', 'log_page' ),
1186 $logs_page_url
1187 );
1188 $pagination = paginate_links(
1189 array(
1190 'base' => add_query_arg( 'log_page', '%#%', $pagination_base ),
1191 'format' => '',
1192 'current' => $current_page,
1193 'total' => $total_pages,
1194 'prev_text' => __( '&laquo; Previous', 'wpvulnerability' ),
1195 'next_text' => __( 'Next &raquo;', 'wpvulnerability' ),
1196 'type' => 'list',
1197 )
1198 );
1199 }
1200
1201 if ( $requested_log > 0 ) {
1202 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( is_string( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '' ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1203 if ( $nonce && wp_verify_nonce( $nonce, 'wpvulnerability_view_log_' . $requested_log ) ) {
1204 $view_log = wpvulnerability_get_api_log( $requested_log );
1205 if ( ! $view_log ) {
1206 $log_error = __( 'Log entry not found or has been removed.', 'wpvulnerability' );
1207 }
1208 } else {
1209 $log_error = __( 'Unable to load the requested log entry.', 'wpvulnerability' );
1210 }
1211 }
1212
1213 ?>
1214 <section class="section wpvulnerability-logs-panel">
1215
1216 <?php if ( ! $view_log ) : ?>
1217 <!-- Configuration Section -->
1218 <div class="wpvulnerability-security-section">
1219 <h3><?php esc_html_e( 'Log Configuration', 'wpvulnerability' ); ?></h3>
1220
1221 <!-- Stats Row -->
1222 <div class="wpvulnerability-stats-row">
1223 <div class="wpvulnerability-stat-card">
1224 <div class="wpvulnerability-stat-label"><?php esc_html_e( 'Total Logs', 'wpvulnerability' ); ?></div>
1225 <div class="wpvulnerability-stat-value"><?php echo esc_html( number_format_i18n( $total_logs ) ); ?></div>
1226 </div>
1227 <div class="wpvulnerability-stat-card">
1228 <div class="wpvulnerability-stat-label"><?php esc_html_e( 'Retention Period', 'wpvulnerability' ); ?></div>
1229 <div class="wpvulnerability-stat-value">
1230 <?php
1231 echo esc_html(
1232 0 === $current
1233 ? __( 'Disabled', 'wpvulnerability' )
1234 : sprintf(
1235 /* translators: %d: Number of days. */
1236 _n( '%d day', '%d days', $current, 'wpvulnerability' ),
1237 $current
1238 )
1239 );
1240 ?>
1241 </div>
1242 </div>
1243 </div>
1244
1245 <!-- Config Form -->
1246 <form method="post" action="" class="wpvulnerability-config-form">
1247 <?php wp_nonce_field( 'wpvulnerability_nonce', 'wpauto_nonce' ); ?>
1248 <label for="wpvulnerability_log_retention">
1249 <?php esc_html_e( 'Log Retention Period', 'wpvulnerability' ); ?>
1250 </label>
1251 <select name="wpvulnerability-config[log_retention]" id="wpvulnerability_log_retention"<?php disabled( null !== $forced ); ?>>
1252 <?php
1253 foreach ( $choices as $days ) {
1254 $label = 0 === $days
1255 ? __( 'None (Disabled)', 'wpvulnerability' )
1256 : sprintf(
1257 /* translators: %d: Number of days. */
1258 _n( '%d day', '%d days', $days, 'wpvulnerability' ),
1259 $days
1260 );
1261 printf(
1262 '<option value="%1$s"%2$s>%3$s</option>',
1263 esc_attr( (string) $days ),
1264 selected( $current, $days, false ),
1265 esc_html( $label )
1266 );
1267 }
1268 ?>
1269 </select>
1270 <?php
1271 if ( null !== $forced ) {
1272 printf(
1273 '<input type="hidden" name="wpvulnerability-config[log_retention]" value="%s" />',
1274 esc_attr( (string) $forced )
1275 );
1276 printf(
1277 '<p class="description">%s</p>',
1278 esc_html__( 'This value is enforced by the WPVULNERABILITY_LOG_RETENTION_DAYS constant.', 'wpvulnerability' )
1279 );
1280 } else {
1281 printf(
1282 '<p class="description">%s</p>',
1283 esc_html__( 'Choose how long WPVulnerability should keep API response logs. Older logs are automatically deleted.', 'wpvulnerability' )
1284 );
1285 }
1286 ?>
1287 <?php submit_button( __( 'Save Configuration', 'wpvulnerability' ), 'primary', 'wpvulnerability_submit', false ); ?>
1288 </form>
1289
1290 <?php if ( 0 === $current ) : ?>
1291 <div class="wpvulnerability-info-box" style="margin-top: 20px;">
1292 <p><strong><?php esc_html_e( 'Log retention is currently disabled.', 'wpvulnerability' ); ?></strong> <?php esc_html_e( 'API responses are not being saved.', 'wpvulnerability' ); ?></p>
1293 </div>
1294 <?php endif; ?>
1295 </div>
1296
1297 <?php if ( $log_error ) : ?>
1298 <div class="notice notice-error"><p><?php echo esc_html( $log_error ); ?></p></div>
1299 <?php endif; ?>
1300 <?php endif; ?>
1301
1302 <?php if ( $view_log ) : ?>
1303 <!-- Log Detail View -->
1304 <div class="wpvulnerability-log-detail">
1305 <h3><?php esc_html_e( 'Log Details', 'wpvulnerability' ); ?></h3>
1306
1307 <div class="wpvulnerability-log-meta">
1308 <div class="wpvulnerability-log-meta-item">
1309 <div class="wpvulnerability-log-meta-label"><?php esc_html_e( 'Date', 'wpvulnerability' ); ?></div>
1310 <div class="wpvulnerability-log-meta-value"><?php echo esc_html( wpvulnerability_format_log_date( $view_log ) ); ?></div>
1311 </div>
1312 <div class="wpvulnerability-log-meta-item">
1313 <div class="wpvulnerability-log-meta-label"><?php esc_html_e( 'API Endpoint', 'wpvulnerability' ); ?></div>
1314 <div class="wpvulnerability-log-meta-value"><?php echo esc_html( get_the_title( $view_log ) ); ?></div>
1315 </div>
1316 </div>
1317
1318 <h4 style="margin: 20px 0 12px 0; color: #1d2327; font-size: 15px;"><?php esc_html_e( 'API Response', 'wpvulnerability' ); ?></h4>
1319 <pre><code><?php echo esc_html( wpvulnerability_format_log_content( $view_log->post_content ) ); ?></code></pre>
1320
1321 <p style="margin-top: 20px;">
1322 <a class="button button-primary" href="<?php echo esc_url( $logs_page_url ); ?>">
1323 &larr; <?php esc_html_e( 'Back to Logs', 'wpvulnerability' ); ?>
1324 </a>
1325 </p>
1326 </div>
1327 <?php else : ?>
1328 <!-- Logs List Section -->
1329 <?php if ( empty( $logs ) ) : ?>
1330 <div class="wpvulnerability-empty-state">
1331 <div class="wpvulnerability-empty-state-icon">📋</div>
1332 <h3><?php esc_html_e( 'No logs available', 'wpvulnerability' ); ?></h3>
1333 <p><?php esc_html_e( 'API response logs will appear here once the vulnerability scanner runs.', 'wpvulnerability' ); ?></p>
1334 </div>
1335 <?php else : ?>
1336 <div class="wpvulnerability-security-section">
1337 <h3><?php esc_html_e( 'API Response Logs', 'wpvulnerability' ); ?></h3>
1338
1339 <!-- Toolbar -->
1340 <div class="wpvulnerability-toolbar">
1341 <div class="wpvulnerability-toolbar-left">
1342 <form method="get" action="<?php echo esc_url( network_admin_url( 'settings.php' ) ); ?>" style="display: flex; align-items: center; gap: 8px; margin: 0;">
1343 <input type="hidden" name="page" value="wpvulnerability-options" />
1344 <input type="hidden" name="tab" value="logs" />
1345 <input type="hidden" name="log_page" value="1" />
1346 <label for="wpvulnerability_logs_per_page" style="margin: 0; font-size: 13px; color: #646970;">
1347 <?php esc_html_e( 'Show:', 'wpvulnerability' ); ?>
1348 </label>
1349 <select name="logs_per_page" id="wpvulnerability_logs_per_page" style="min-width: 80px;">
1350 <?php foreach ( $per_page_options as $per_page_option ) : ?>
1351 <option value="<?php echo esc_attr( (string) $per_page_option ); ?>"<?php selected( $logs_per_page, $per_page_option ); ?>>
1352 <?php echo esc_html( number_format_i18n( $per_page_option ) ); ?>
1353 </option>
1354 <?php endforeach; ?>
1355 </select>
1356 <?php submit_button( __( 'Apply', 'wpvulnerability' ), 'secondary', 'submit', false, array( 'style' => 'margin: 0; padding: 4px 12px; height: auto;' ) ); ?>
1357 </form>
1358 </div>
1359 <div class="wpvulnerability-toolbar-right">
1360 <span style="font-size: 13px; color: #646970;">
1361 <?php
1362 printf(
1363 /* translators: 1: first item, 2: last item, 3: total items */
1364 esc_html__( 'Showing %1$s–%2$s of %3$s', 'wpvulnerability' ),
1365 esc_html( number_format_i18n( ( ( $current_page - 1 ) * $logs_per_page ) + 1 ) ),
1366 esc_html( number_format_i18n( min( $current_page * $logs_per_page, $total_logs ) ) ),
1367 esc_html( number_format_i18n( $total_logs ) )
1368 );
1369 ?>
1370 </span>
1371 </div>
1372 </div>
1373
1374 <!-- Logs Table -->
1375 <table class="wpvulnerability-logs-table">
1376 <thead>
1377 <tr>
1378 <th style="width: 200px;"><?php esc_html_e( 'Date', 'wpvulnerability' ); ?></th>
1379 <th><?php esc_html_e( 'API Endpoint', 'wpvulnerability' ); ?></th>
1380 <th style="width: 100px; text-align: center;"><?php esc_html_e( 'Actions', 'wpvulnerability' ); ?></th>
1381 </tr>
1382 </thead>
1383 <tbody>
1384 <?php foreach ( $logs as $log ) : ?>
1385 <tr>
1386 <td>
1387 <span class="wpvulnerability-log-date"><?php echo esc_html( wpvulnerability_format_log_date( $log ) ); ?></span>
1388 </td>
1389 <td>
1390 <span class="wpvulnerability-log-url"><?php echo esc_html( get_the_title( $log ) ); ?></span>
1391 </td>
1392 <td style="text-align: center;">
1393 <?php
1394 $view_url = add_query_arg(
1395 array(
1396 'page' => 'wpvulnerability-options',
1397 'tab' => 'logs',
1398 'log' => $log->ID,
1399 'logs_per_page' => $logs_per_page,
1400 'log_page' => $current_page,
1401 ),
1402 network_admin_url( 'settings.php' )
1403 );
1404 $view_url = wp_nonce_url( $view_url, 'wpvulnerability_view_log_' . $log->ID );
1405 ?>
1406 <a class="button button-small" href="<?php echo esc_url( $view_url ); ?>">
1407 <?php esc_html_e( 'View', 'wpvulnerability' ); ?>
1408 </a>
1409 </td>
1410 </tr>
1411 <?php endforeach; ?>
1412 </tbody>
1413 </table>
1414
1415 <!-- Pagination -->
1416 <?php if ( $pagination ) : ?>
1417 <div class="wpvulnerability-pagination">
1418 <?php echo wp_kses_post( $pagination ); ?>
1419 </div>
1420 <?php endif; ?>
1421
1422 <!-- Danger Zone -->
1423 <div class="wpvulnerability-danger-zone">
1424 <h4><?php esc_html_e( 'Danger Zone', 'wpvulnerability' ); ?></h4>
1425 <p style="margin: 0 0 12px 0; font-size: 13px; color: #646970;">
1426 <?php esc_html_e( 'Permanently delete all API response logs. This action cannot be undone.', 'wpvulnerability' ); ?>
1427 </p>
1428 <form method="post" action="" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to delete all logs? This action cannot be undone.', 'wpvulnerability' ) ); ?>');">
1429 <?php wp_nonce_field( 'wpvulnerability_delete_logs_action', 'wpvulnerability_delete_logs_nonce' ); ?>
1430 <input type="hidden" name="wpvulnerability_delete_logs" value="1" />
1431 <?php submit_button( __( 'Delete All Logs', 'wpvulnerability' ), 'delete', 'submit', false ); ?>
1432 </form>
1433 </div>
1434 </div>
1435 <?php endif; ?>
1436 <?php endif; ?>
1437 </section>
1438 <?php
1439 }
1440
1441 /**
1442 * Outputs the Tools tab contents for the multisite settings screen.
1443 *
1444 * @since 4.1.2
1445 *
1446 * @return void
1447 */
1448 function wpvulnerability_render_network_admin_tab_tools() {
1449 global $wpvulnerability_settings;
1450
1451 $wpvulnerability_cron_config = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
1452 if ( ! is_array( $wpvulnerability_cron_config ) ) {
1453 $wpvulnerability_cron_config = array();
1454 }
1455 $is_main_site = ( ! is_multisite() || is_main_site() );
1456 $cron_status = wpvulnerability_get_cron_status( $wpvulnerability_cron_config, $is_main_site );
1457 $cron_schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : array();
1458 $_df = get_option( 'date_format' );
1459 $_tf = get_option( 'time_format' );
1460 $date_format = ( is_scalar( $_df ) ? (string) $_df : '' ) . ' ' . ( is_scalar( $_tf ) ? (string) $_tf : '' );
1461 ?>
1462 <section class="section wpvulnerability-tools-panel">
1463
1464 <!-- Reload Data Tool -->
1465 <div class="wpvulnerability-tool-card">
1466 <h3>
1467 <span class="wpvulnerability-tool-icon">🔄</span>
1468 <?php esc_html_e( 'Reload Data from API', 'wpvulnerability' ); ?>
1469 </h3>
1470 <p class="wpvulnerability-tool-description">
1471 <?php esc_html_e( 'Reload all Core, Plugins, Themes and other components information directly from the API to have updated data. This will clear the cache and fetch fresh vulnerability information.', 'wpvulnerability' ); ?>
1472 </p>
1473 <div class="wpvulnerability-tool-actions">
1474 <form method="post" action="" style="display: inline;">
1475 <?php wp_nonce_field( 'wpvulnerability_reset_action', 'wpvulnerability_reset_nonce' ); ?>
1476 <input type="submit" name="wpvulnerability_reset" value="<?php esc_attr_e( 'Reload Data', 'wpvulnerability' ); ?>" class="button button-primary">
1477 </form>
1478 </div>
1479 </div>
1480
1481 <!-- Email Test Tool -->
1482 <div class="wpvulnerability-tool-card">
1483 <h3>
1484 <span class="wpvulnerability-tool-icon">📧</span>
1485 <?php esc_html_e( 'Test Email Notifications', 'wpvulnerability' ); ?>
1486 </h3>
1487 <p class="wpvulnerability-tool-description">
1488 <?php esc_html_e( 'Send a test email notification with current vulnerability data to verify your email configuration is working correctly.', 'wpvulnerability' ); ?>
1489 </p>
1490 <?php
1491 $from_email = null;
1492 if ( defined( 'WPVULNERABILITY_MAIL' ) ) {
1493 $from_email = sanitize_email( trim( (string) WPVULNERABILITY_MAIL ) );
1494 if ( is_email( $from_email ) ) {
1495 ?>
1496 <div class="wpvulnerability-info-box">
1497 <p>
1498 <strong><?php esc_html_e( 'From address (configured via constant):', 'wpvulnerability' ); ?></strong>
1499 <code><?php echo esc_html( $from_email ); ?></code>
1500 </p>
1501 </div>
1502 <?php
1503 }
1504 }
1505 if ( ! $from_email ) {
1506 $from_email = get_bloginfo( 'admin_email' );
1507 ?>
1508 <div class="wpvulnerability-info-box">
1509 <p>
1510 <strong><?php esc_html_e( 'From address (default):', 'wpvulnerability' ); ?></strong>
1511 <code><?php echo esc_html( $from_email ); ?></code>
1512 </p>
1513 </div>
1514 <?php
1515 }
1516 ?>
1517 <p style="margin: 8px 0; font-size: 13px;">
1518 <a href="https://www.wpvulnerability.com/plugin/#from-mail" target="_blank" rel="noopener noreferrer">
1519 <?php esc_html_e( 'Learn how to customize the email sender address →', 'wpvulnerability' ); ?>
1520 </a>
1521 </p>
1522 <div class="wpvulnerability-tool-actions">
1523 <form method="post" action="" style="display: inline;">
1524 <?php wp_nonce_field( 'wpvulnerability_email_action', 'wpvulnerability_email_nonce' ); ?>
1525 <input type="submit" name="wpvulnerability_email" value="<?php esc_attr_e( 'Send Test Email', 'wpvulnerability' ); ?>" class="button button-primary">
1526 </form>
1527 </div>
1528 </div>
1529
1530 <!-- WP-Cron Status Tool -->
1531 <div class="wpvulnerability-tool-card">
1532 <h3>
1533 <span class="wpvulnerability-tool-icon"></span>
1534 <?php esc_html_e( 'WP-Cron Status', 'wpvulnerability' ); ?>
1535 </h3>
1536 <p class="wpvulnerability-tool-description">
1537 <?php esc_html_e( 'View and manage WP-Cron scheduled events. Compare expected schedules with actual cron jobs to ensure automated tasks are running correctly.', 'wpvulnerability' ); ?>
1538 </p>
1539
1540 <?php if ( empty( $cron_status['expected'] ) ) : ?>
1541 <div class="wpvulnerability-info-box">
1542 <p><?php esc_html_e( 'No cron data available.', 'wpvulnerability' ); ?></p>
1543 </div>
1544 <?php else : ?>
1545 <table class="wpvulnerability-cron-table">
1546 <thead>
1547 <tr>
1548 <th><?php esc_html_e( 'Hook', 'wpvulnerability' ); ?></th>
1549 <th><?php esc_html_e( 'Expected', 'wpvulnerability' ); ?></th>
1550 <th><?php esc_html_e( 'Found', 'wpvulnerability' ); ?></th>
1551 <th><?php esc_html_e( 'Next Run', 'wpvulnerability' ); ?></th>
1552 <th><?php esc_html_e( 'Status', 'wpvulnerability' ); ?></th>
1553 </tr>
1554 </thead>
1555 <tbody>
1556 <?php foreach ( $cron_status['expected'] as $row ) : ?>
1557 <?php
1558 $row_hook = isset( $row['hook'] ) && is_scalar( $row['hook'] ) ? (string) $row['hook'] : '';
1559 $row_label = isset( $row['label'] ) && is_scalar( $row['label'] ) ? (string) $row['label'] : $row_hook;
1560 $row_should_exist = ! empty( $row['should_exist'] );
1561 $row_count = isset( $row['count'] ) && is_scalar( $row['count'] ) ? (int) $row['count'] : 0;
1562 $row_next_run_raw = isset( $row['next_run'] ) ? $row['next_run'] : null;
1563 $expected_schedule = isset( $row['schedule'] ) && is_scalar( $row['schedule'] ) ? (string) $row['schedule'] : '';
1564 $schedule_label = '';
1565 if ( ! $row_should_exist ) {
1566 $schedule_label = __( 'Not expected', 'wpvulnerability' );
1567 } elseif ( '' === $expected_schedule ) {
1568 $schedule_label = __( 'Disabled', 'wpvulnerability' );
1569 } elseif ( isset( $cron_schedules[ $expected_schedule ]['display'] ) ) {
1570 $schedule_label = $cron_schedules[ $expected_schedule ]['display'];
1571 } else {
1572 $schedule_label = $expected_schedule;
1573 }
1574
1575 $found_schedules = array();
1576 $row_schedules_found = isset( $row['schedules_found'] ) && is_array( $row['schedules_found'] ) ? $row['schedules_found'] : array();
1577 if ( ! empty( $row_schedules_found ) ) {
1578 foreach ( $row_schedules_found as $schedule_id ) {
1579 $schedule_id = is_scalar( $schedule_id ) ? (string) $schedule_id : '';
1580 if ( isset( $cron_schedules[ $schedule_id ]['display'] ) ) {
1581 $found_schedules[] = $cron_schedules[ $schedule_id ]['display'];
1582 } else {
1583 $found_schedules[] = $schedule_id;
1584 }
1585 }
1586 }
1587
1588 $found_label = $row_count > 0 ? implode( ', ', $found_schedules ) : __( 'None', 'wpvulnerability' );
1589 $next_run = ( $row_next_run_raw && is_numeric( $row_next_run_raw ) ) ? date_i18n( $date_format, (int) $row_next_run_raw ) : '';
1590 $row_messages = isset( $row['messages'] ) && is_array( $row['messages'] ) ? $row['messages'] : array();
1591 $status_text = implode(
1592 ' ',
1593 array_map(
1594 static function ( $m ) {
1595 return is_scalar( $m ) ? (string) $m : '';
1596 },
1597 $row_messages
1598 )
1599 );
1600
1601 // Determine status badge class.
1602 $status_class = 'success';
1603 if ( strpos( $status_text, 'Missing' ) !== false || strpos( $status_text, 'Duplicate' ) !== false ) {
1604 $status_class = 'error';
1605 } elseif ( strpos( $status_text, 'legacy' ) !== false ) {
1606 $status_class = 'warning';
1607 }
1608 ?>
1609 <tr>
1610 <td>
1611 <strong><?php echo esc_html( $row_label ); ?></strong><br>
1612 <code><?php echo esc_html( $row_hook ); ?></code>
1613 </td>
1614 <td><?php echo esc_html( $schedule_label ); ?></td>
1615 <td><?php echo esc_html( $found_label ); ?></td>
1616 <td><?php echo esc_html( $next_run ); ?></td>
1617 <td>
1618 <?php if ( $status_text ) : ?>
1619 <span class="wpvulnerability-status-badge <?php echo esc_attr( $status_class ); ?>">
1620 <?php echo esc_html( $status_text ); ?>
1621 </span>
1622 <?php else : ?>
1623 <span class="wpvulnerability-status-badge success"> <?php esc_html_e( 'OK', 'wpvulnerability' ); ?></span>
1624 <?php endif; ?>
1625 </td>
1626 </tr>
1627 <?php endforeach; ?>
1628 </tbody>
1629 </table>
1630
1631 <?php if ( ! empty( $cron_status['unexpected'] ) ) : ?>
1632 <div class="wpvulnerability-unexpected-list">
1633 <h4><?php esc_html_e( '⚠️ Unexpected WPVulnerability Events', 'wpvulnerability' ); ?></h4>
1634 <ul>
1635 <?php foreach ( $cron_status['unexpected'] as $unexpected ) : ?>
1636 <?php
1637 $unexpected_hook = isset( $unexpected['hook'] ) && is_scalar( $unexpected['hook'] ) ? (string) $unexpected['hook'] : '';
1638 $unexpected_schedules = array();
1639 $unexpected_sched_raw = isset( $unexpected['schedules'] ) && is_array( $unexpected['schedules'] ) ? $unexpected['schedules'] : array();
1640 if ( ! empty( $unexpected_sched_raw ) ) {
1641 foreach ( $unexpected_sched_raw as $schedule_id ) {
1642 $schedule_id = is_scalar( $schedule_id ) ? (string) $schedule_id : '';
1643 if ( isset( $cron_schedules[ $schedule_id ]['display'] ) ) {
1644 $unexpected_schedules[] = $cron_schedules[ $schedule_id ]['display'];
1645 } else {
1646 $unexpected_schedules[] = $schedule_id;
1647 }
1648 }
1649 }
1650 $unexpected_label = empty( $unexpected_schedules ) ? __( 'No interval', 'wpvulnerability' ) : implode( ', ', $unexpected_schedules );
1651 $unexpected_next_raw = isset( $unexpected['next_run'] ) ? $unexpected['next_run'] : null;
1652 $unexpected_next = ( $unexpected_next_raw && is_numeric( $unexpected_next_raw ) ) ? date_i18n( $date_format, (int) $unexpected_next_raw ) : '';
1653 ?>
1654 <li>
1655 <code><?php echo esc_html( $unexpected_hook ); ?></code>
1656 <?php echo esc_html( $unexpected_label ); ?>
1657 <?php esc_html_e( 'Next:', 'wpvulnerability' ); ?> <?php echo esc_html( $unexpected_next ); ?>
1658 </li>
1659 <?php endforeach; ?>
1660 </ul>
1661 </div>
1662 <?php endif; ?>
1663
1664 <div class="wpvulnerability-tool-actions">
1665 <form method="post" action="" style="display: inline;">
1666 <?php wp_nonce_field( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ); ?>
1667 <input type="submit" name="wpvulnerability_repair_cron" value="<?php esc_attr_e( 'Repair Cron Events', 'wpvulnerability' ); ?>" class="button button-secondary">
1668 </form>
1669 </div>
1670 <?php endif; ?>
1671 </div>
1672
1673 <!-- Uninstall Data Preference -->
1674 <div class="wpvulnerability-tool-card">
1675 <h4><?php esc_html_e( 'Uninstall Data', 'wpvulnerability' ); ?></h4>
1676 <p style="margin: 0 0 12px 0; color: #646970; font-size: 13px; line-height: 1.6;">
1677 <?php esc_html_e( 'By default, all plugin data is preserved when WPVulnerability is deleted. Enable the option below only if you want all options, logs, and cached data permanently removed on uninstall.', 'wpvulnerability' ); ?>
1678 </p>
1679 <form method="post" action="">
1680 <?php wp_nonce_field( 'wpvulnerability_uninstall_action', 'wpvulnerability_uninstall_nonce' ); ?>
1681 <?php
1682 $current_config = get_site_option( 'wpvulnerability-config', array() );
1683 $delete_on_uninstall = is_array( $current_config ) && ! empty( $current_config['delete_on_uninstall'] );
1684 ?>
1685 <label>
1686 <input type="checkbox" name="delete_on_uninstall" value="1" <?php checked( $delete_on_uninstall ); ?> />
1687 <strong style="color: #b32d2e;"><?php esc_html_e( 'Delete all plugin data on uninstall', 'wpvulnerability' ); ?></strong>
1688 </label>
1689 <p style="margin: 8px 0 12px; color: #646970; font-size: 12px;">
1690 <?php esc_html_e( 'Default: unchecked (data preserved). Check only if you want a clean removal.', 'wpvulnerability' ); ?>
1691 </p>
1692 <input type="submit" name="wpvulnerability_delete_on_uninstall" value="<?php esc_attr_e( 'Save preference', 'wpvulnerability' ); ?>" class="button button-secondary">
1693 </form>
1694 </div>
1695
1696 <!-- Reset Plugin Tool -->
1697 <div class="wpvulnerability-tool-card">
1698 <div class="wpvulnerability-danger-zone">
1699 <h4>⚠️ <?php esc_html_e( 'Reset WPVulnerability', 'wpvulnerability' ); ?></h4>
1700 <p style="margin: 0 0 16px 0; color: #646970; font-size: 13px; line-height: 1.6;">
1701 <?php esc_html_e( 'Delete all WPVulnerability settings, cached data, logs, and scheduled events, then restore defaults and reload data from the API.', 'wpvulnerability' ); ?>
1702 <br>
1703 <strong style="color: #b32d2e;"><?php esc_html_e( 'This action cannot be undone.', 'wpvulnerability' ); ?></strong>
1704 </p>
1705 <form method="post" action="" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset all WPVulnerability data? This action cannot be undone.', 'wpvulnerability' ) ); ?>');">
1706 <?php wp_nonce_field( 'wpvulnerability_full_reset_action', 'wpvulnerability_full_reset_nonce' ); ?>
1707 <input type="submit" name="wpvulnerability_full_reset" value="<?php esc_attr_e( 'Reset Plugin', 'wpvulnerability' ); ?>" class="button button-delete">
1708 </form>
1709 </div>
1710 </div>
1711 </section>
1712 <?php
1713 }
1714
1715 /**
1716 * Outputs the About tab contents for the multisite settings screen.
1717 *
1718 * @since 4.1.2
1719 *
1720 * @return void
1721 */
1722 function wpvulnerability_render_network_admin_tab_about() {
1723 $raw_statistics = get_site_option( 'wpvulnerability-statistics', '' );
1724 $wpvulnerability_statistics = json_decode( is_string( $raw_statistics ) ? $raw_statistics : '', true );
1725 if ( ! is_array( $wpvulnerability_statistics ) ) {
1726 $wpvulnerability_statistics = array();
1727 }
1728
1729 ?>
1730 <section class="section wpvulnerability-about-panel">
1731
1732 <!-- Database Statistics Section -->
1733 <div class="wpvulnerability-security-section">
1734 <h3><?php esc_html_e( 'Vulnerability Database Statistics', 'wpvulnerability' ); ?></h3>
1735
1736 <?php
1737 $components = array(
1738 'plugins' => __( 'Plugins', 'wpvulnerability' ),
1739 'themes' => __( 'Themes', 'wpvulnerability' ),
1740 'php' => __( 'PHP', 'wpvulnerability' ),
1741 'apache' => __( 'Apache HTTPD', 'wpvulnerability' ),
1742 'nginx' => __( 'nginx', 'wpvulnerability' ),
1743 'mariadb' => __( 'MariaDB', 'wpvulnerability' ),
1744 'mysql' => __( 'MySQL', 'wpvulnerability' ),
1745 'imagemagick' => __( 'ImageMagick', 'wpvulnerability' ),
1746 'curl' => __( 'curl', 'wpvulnerability' ),
1747 'memcached' => __( 'memcached', 'wpvulnerability' ),
1748 'redis' => __( 'Redis', 'wpvulnerability' ),
1749 'sqlite' => __( 'SQLite', 'wpvulnerability' ),
1750 );
1751 ?>
1752
1753 <div class="wpvulnerability-stats-grid">
1754 <?php foreach ( $components as $component => $label ) : ?>
1755 <?php
1756 $component_data = isset( $wpvulnerability_statistics[ $component ] ) && is_array( $wpvulnerability_statistics[ $component ] ) ? $wpvulnerability_statistics[ $component ] : null;
1757 $has_data = null !== $component_data;
1758 $card_class = 'wpvulnerability-stat-card';
1759 if ( ! $has_data ) {
1760 $card_class .= ' wpvulnerability-no-data';
1761 }
1762 ?>
1763 <div class="<?php echo esc_attr( $card_class ); ?>">
1764 <div class="wpvulnerability-stat-label"><?php echo esc_html( $label ); ?></div>
1765 <?php if ( $has_data ) : ?>
1766 <?php
1767 $vuln_count = absint( isset( $component_data['vulnerabilities'] ) && is_scalar( $component_data['vulnerabilities'] ) ? (int) $component_data['vulnerabilities'] : 0 );
1768 ?>
1769 <div class="wpvulnerability-stat-value">
1770 <?php echo esc_html( number_format_i18n( $vuln_count ) ); ?>
1771 </div>
1772 <div class="wpvulnerability-stat-meta">
1773 <?php
1774 printf(
1775 // translators: number of vulnerabilities.
1776 esc_html( _n( '%s vulnerability', '%s vulnerabilities', $vuln_count, 'wpvulnerability' ) ),
1777 ''
1778 );
1779 ?>
1780 <?php if ( isset( $component_data['products'] ) ) : ?>
1781 <?php $prod_count = absint( is_scalar( $component_data['products'] ) ? (int) $component_data['products'] : 0 ); ?>
1782 <br>
1783 <?php
1784 printf(
1785 // translators: number of products.
1786 esc_html( _n( '(%s product)', '(%s products)', $prod_count, 'wpvulnerability' ) ),
1787 esc_html( number_format_i18n( $prod_count ) )
1788 );
1789 ?>
1790 <?php endif; ?>
1791 </div>
1792 <?php else : ?>
1793 <div class="wpvulnerability-stat-value"><?php esc_html_e( 'No data', 'wpvulnerability' ); ?></div>
1794 <?php endif; ?>
1795 </div>
1796 <?php endforeach; ?>
1797 </div>
1798
1799 <?php if ( isset( $wpvulnerability_statistics['updated'] ) ) : ?>
1800 <div class="wpvulnerability-info-box">
1801 <p>
1802 <?php
1803 switch_to_locale( determine_locale() );
1804 $updated_data = is_array( $wpvulnerability_statistics['updated'] ) ? $wpvulnerability_statistics['updated'] : array();
1805 $updated_epoch = isset( $updated_data['unixepoch'] ) && is_scalar( $updated_data['unixepoch'] ) ? (int) $updated_data['unixepoch'] : 0;
1806 $_upd_df = get_option( 'date_format' );
1807 $_upd_tf = get_option( 'time_format' );
1808 $formatted_datetime = date_i18n( ( is_scalar( $_upd_df ) ? (string) $_upd_df : '' ) . ' ' . ( is_scalar( $_upd_tf ) ? (string) $_upd_tf : '' ), $updated_epoch );
1809 // translators: date of last update.
1810 printf( esc_html__( 'Database last updated: %s', 'wpvulnerability' ), '<strong>' . esc_html( $formatted_datetime ) . '</strong>' );
1811 restore_previous_locale();
1812 ?>
1813 </p>
1814 </div>
1815 <?php endif; ?>
1816 </div>
1817
1818 <!-- Intelligence Sources Section -->
1819 <?php
1820 $sources_stat = isset( $wpvulnerability_statistics['sources'] ) && is_array( $wpvulnerability_statistics['sources'] ) ? $wpvulnerability_statistics['sources'] : array();
1821 $source_labels = array(
1822 'cve' => 'CVE',
1823 'euvd' => 'EUVD',
1824 'jvn' => 'JVN',
1825 'patchstack' => 'Patchstack',
1826 'wpscan' => 'WPScan',
1827 'wordfence' => 'Wordfence',
1828 );
1829 if ( ! empty( $sources_stat ) ) :
1830 ?>
1831 <div class="wpvulnerability-security-section">
1832 <h3><?php esc_html_e( 'Intelligence Sources', 'wpvulnerability' ); ?></h3>
1833 <p><?php esc_html_e( 'Number of vulnerabilities contributed by each intelligence source.', 'wpvulnerability' ); ?></p>
1834 <table class="wp-list-table widefat wpvulnerability">
1835 <thead>
1836 <tr>
1837 <th><?php esc_html_e( 'Source', 'wpvulnerability' ); ?></th>
1838 <th><?php esc_html_e( 'Core', 'wpvulnerability' ); ?></th>
1839 <th><?php esc_html_e( 'Plugins', 'wpvulnerability' ); ?></th>
1840 <th><?php esc_html_e( 'Themes', 'wpvulnerability' ); ?></th>
1841 </tr>
1842 </thead>
1843 <tbody>
1844 <?php foreach ( $source_labels as $src_key => $src_label ) : ?>
1845 <?php
1846 if ( ! isset( $sources_stat[ $src_key ] ) || ! is_array( $sources_stat[ $src_key ] ) ) {
1847 continue; }
1848 ?>
1849 <?php
1850 $src_row = $sources_stat[ $src_key ];
1851 $s_core = absint( is_scalar( $src_row['core'] ?? 0 ) ? (int) ( $src_row['core'] ?? 0 ) : 0 );
1852 $s_plugins = absint( is_scalar( $src_row['plugins'] ?? 0 ) ? (int) ( $src_row['plugins'] ?? 0 ) : 0 );
1853 $s_themes = absint( is_scalar( $src_row['themes'] ?? 0 ) ? (int) ( $src_row['themes'] ?? 0 ) : 0 );
1854 ?>
1855 <tr>
1856 <td><strong><?php echo esc_html( $src_label ); ?></strong></td>
1857 <td><?php echo esc_html( number_format_i18n( $s_core ) ); ?></td>
1858 <td><?php echo esc_html( number_format_i18n( $s_plugins ) ); ?></td>
1859 <td><?php echo esc_html( number_format_i18n( $s_themes ) ); ?></td>
1860 </tr>
1861 <?php endforeach; ?>
1862 </tbody>
1863 </table>
1864 </div>
1865 <?php endif; ?>
1866
1867 <!-- Sponsors Section -->
1868 <div class="wpvulnerability-security-section">
1869 <h3><?php esc_html_e( 'Sponsors', 'wpvulnerability' ); ?></h3>
1870
1871 <?php
1872 $sponsors_list = isset( $wpvulnerability_statistics['sponsors'] ) && is_array( $wpvulnerability_statistics['sponsors'] ) ? $wpvulnerability_statistics['sponsors'] : array();
1873 ?>
1874 <?php if ( ! empty( $sponsors_list ) ) : ?>
1875 <div class="wpvulnerability-people-grid">
1876 <?php foreach ( $sponsors_list as $sponsor ) : ?>
1877 <?php
1878 if ( ! is_array( $sponsor ) ) {
1879 continue; }
1880 ?>
1881 <div class="wpvulnerability-person-card">
1882 <img src="<?php echo esc_url( isset( $sponsor['image'] ) && is_scalar( $sponsor['image'] ) ? (string) $sponsor['image'] : '' ); ?>" alt="<?php echo esc_attr( isset( $sponsor['name'] ) && is_scalar( $sponsor['name'] ) ? (string) $sponsor['name'] : '' ); ?>">
1883 <a href="<?php echo esc_url( isset( $sponsor['url'] ) && is_scalar( $sponsor['url'] ) ? (string) $sponsor['url'] : '' ); ?>" target="_blank" rel="noreferrer noopener">
1884 <?php echo esc_html( isset( $sponsor['name'] ) && is_scalar( $sponsor['name'] ) ? (string) $sponsor['name'] : '' ); ?>
1885 </a>
1886 </div>
1887 <?php endforeach; ?>
1888 </div>
1889 <?php else : ?>
1890 <div class="wpvulnerability-empty-state">
1891 <div class="wpvulnerability-empty-state-icon">💎</div>
1892 <p><strong><?php esc_html_e( 'No sponsor data available.', 'wpvulnerability' ); ?></strong></p>
1893 </div>
1894 <?php endif; ?>
1895 </div>
1896
1897 <!-- Contributors Section -->
1898 <div class="wpvulnerability-security-section">
1899 <h3><?php esc_html_e( 'Contributors', 'wpvulnerability' ); ?></h3>
1900
1901 <?php
1902 $contributors_list = isset( $wpvulnerability_statistics['contributors'] ) && is_array( $wpvulnerability_statistics['contributors'] ) ? $wpvulnerability_statistics['contributors'] : array();
1903 ?>
1904 <?php if ( ! empty( $contributors_list ) ) : ?>
1905 <div class="wpvulnerability-people-grid">
1906 <?php foreach ( $contributors_list as $contributor ) : ?>
1907 <?php
1908 if ( ! is_array( $contributor ) ) {
1909 continue; }
1910 ?>
1911 <div class="wpvulnerability-person-card">
1912 <img src="<?php echo esc_url( isset( $contributor['image'] ) && is_scalar( $contributor['image'] ) ? (string) $contributor['image'] : '' ); ?>" alt="<?php echo esc_attr( isset( $contributor['name'] ) && is_scalar( $contributor['name'] ) ? (string) $contributor['name'] : '' ); ?>">
1913 <a href="<?php echo esc_url( isset( $contributor['url'] ) && is_scalar( $contributor['url'] ) ? (string) $contributor['url'] : '' ); ?>" target="_blank" rel="noreferrer noopener">
1914 <?php echo esc_html( isset( $contributor['name'] ) && is_scalar( $contributor['name'] ) ? (string) $contributor['name'] : '' ); ?>
1915 </a>
1916 </div>
1917 <?php endforeach; ?>
1918 </div>
1919 <?php else : ?>
1920 <div class="wpvulnerability-empty-state">
1921 <div class="wpvulnerability-empty-state-icon">👥</div>
1922 <p><strong><?php esc_html_e( 'No contributor data available.', 'wpvulnerability' ); ?></strong></p>
1923 </div>
1924 <?php endif; ?>
1925 </div>
1926 </section>
1927 <?php
1928 }
1929
1930 /**
1931 * Adds a WP-Admin menu option for the WPVulnerability plugin.
1932 *
1933 * @since 2.0.0
1934 *
1935 * @return void
1936 */
1937 function wpvulnerability_admin_menu() {
1938 // Adds a submenu page under the Settings menu.
1939 add_submenu_page(
1940 'settings.php',
1941 __( 'WPVulnerability', 'wpvulnerability' ),
1942 __( 'WPVulnerability', 'wpvulnerability' ),
1943 'manage_network_options',
1944 'wpvulnerability-options',
1945 'wpvulnerability_create_admin_page'
1946 );
1947 }
1948 add_action( 'network_admin_menu', 'wpvulnerability_admin_menu' );
1949
1950 /**
1951 * Print the settings header information for the notifications section.
1952 *
1953 * @since 2.0.0
1954 *
1955 * @return void
1956 */
1957 function wpvulnerability_admin_section_notifications() {
1958 // Output the header information for the notifications section.
1959 esc_html_e( 'Configure and save these settings to receive notifications.', 'wpvulnerability' );
1960 }
1961
1962 /**
1963 * Callback function to display the email input field in the admin settings page.
1964 * This function retrieves the current WPVulnerability plugin settings and displays the email input field
1965 * for users to enter their email addresses. If no email is saved in the settings, the admin email is displayed.
1966 *
1967 * @since 2.0.0
1968 *
1969 * @return void
1970 */
1971 function wpvulnerability_admin_emails_callback() {
1972 // Retrieve the WPVulnerability plugin settings.
1973 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config' );
1974 if ( ! is_array( $wpvulnerability_settings ) ) {
1975 $wpvulnerability_settings = array();
1976 }
1977
1978 // Set a default value for the email input field if no email is saved in the settings.
1979 if ( ! isset( $wpvulnerability_settings['emails'] ) ) {
1980 $wpvulnerability_settings['emails'] = '';
1981 }
1982
1983 // Output the email input field. Use the network admin email as a placeholder in a multisite environment.
1984 $admin_email = get_site_option( 'admin_email' );
1985
1986 // Output the email input field and display the admin email as a hint.
1987 ?>
1988 <input class="regular-text" type="text" name="wpvulnerability-config[emails]" id="wpvulnerability_emails" placeholder="<?php echo esc_attr( is_scalar( $admin_email ) ? (string) $admin_email : '' ); ?>" value="<?php echo esc_attr( is_scalar( $wpvulnerability_settings['emails'] ) ? (string) $wpvulnerability_settings['emails'] : '' ); ?>">
1989 <br><small><?php esc_html_e( 'Default administrator email', 'wpvulnerability' ); ?>: <?php echo esc_attr( is_scalar( $admin_email ) ? (string) $admin_email : '' ); ?></small>
1990 <?php
1991
1992 unset( $admin_email );
1993 }
1994
1995 /**
1996 * Print the cache expiration selector.
1997 *
1998 * @since 4.1.0
1999 *
2000 * @return void
2001 */
2002 function wpvulnerability_admin_cache_callback() {
2003
2004 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
2005 if ( ! is_array( $wpvulnerability_settings ) ) {
2006 $wpvulnerability_settings = array();
2007 }
2008 $options = array( 1, 6, 12, 24 );
2009 $forced_cache = null;
2010
2011 if ( defined( 'WPVULNERABILITY_CACHE_HOURS' ) ) {
2012 $forced_cache = (int) WPVULNERABILITY_CACHE_HOURS;
2013 if ( ! in_array( $forced_cache, $options, true ) ) {
2014 $options[] = $forced_cache;
2015 sort( $options, SORT_NUMERIC );
2016 }
2017 }
2018
2019 $current = isset( $wpvulnerability_settings['cache'] ) && is_scalar( $wpvulnerability_settings['cache'] ) ? (int) $wpvulnerability_settings['cache'] : 12;
2020 if ( null !== $forced_cache ) {
2021 $current = $forced_cache;
2022 }
2023
2024 echo '<select name="wpvulnerability-config[cache]" id="wpvulnerability_cache"';
2025 disabled( null !== $forced_cache );
2026 echo '>';
2027 foreach ( $options as $hours ) {
2028 printf(
2029 '<option value="%1$s"%2$s>%3$s</option>',
2030 esc_attr( (string) $hours ),
2031 selected( $current, $hours, false ),
2032 esc_html(
2033 sprintf(
2034 /* translators: %d: number of hours */
2035 _n( '%d hour', '%d hours', $hours, 'wpvulnerability' ),
2036 $hours
2037 )
2038 )
2039 );
2040 }
2041 echo '</select>';
2042
2043 if ( null !== $forced_cache ) {
2044 printf(
2045 '<input type="hidden" name="wpvulnerability-config[cache]" value="%s" />',
2046 esc_attr( (string) $current )
2047 );
2048 }
2049
2050 printf(
2051 '<p class="description"><a href="%1$s" target="_blank"><small><i>%2$s</i></small></a></p>',
2052 esc_url( 'https://www.wpvulnerability.com/plugin/#cache-duration' ),
2053 esc_html__( 'Read more if you want to force the cache time.', 'wpvulnerability' )
2054 );
2055 }
2056
2057
2058 /**
2059 * Print the settings header information for the analyze section.
2060 *
2061 * @since 3.3.0
2062 *
2063 * @return void
2064 */
2065 function wpvulnerability_admin_section_analyze() {
2066 // Output the header information for the analyze section.
2067 esc_html_e( 'Configure and save these settings to hide vulnerabilities.', 'wpvulnerability' );
2068 }
2069
2070 /**
2071 * Print when to send the vulnerability scan emails.
2072 *
2073 * @since 2.0.0
2074 *
2075 * @return void
2076 */
2077 function wpvulnerability_admin_period_callback() {
2078 // Get the saved plugin settings.
2079 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
2080 if ( ! is_array( $wpvulnerability_settings ) ) {
2081 $wpvulnerability_settings = array();
2082 }
2083 $defaults = array(
2084 'period' => 'weekly',
2085 'day' => 'monday',
2086 'hour' => 0,
2087 'minute' => 0,
2088 );
2089 $wpvulnerability_settings = wp_parse_args( $wpvulnerability_settings, $defaults );
2090
2091 ?>
2092 <div id="wpvulnerability_period">
2093 <label>
2094 <input type="radio" name="wpvulnerability-config[period]" value="never" <?php checked( $wpvulnerability_settings['period'], 'never' ); ?> />
2095 <?php esc_html_e( 'Never', 'wpvulnerability' ); ?>
2096 </label>
2097 <br/>
2098 <label>
2099 <input type="radio" name="wpvulnerability-config[period]" value="daily" <?php checked( $wpvulnerability_settings['period'], 'daily' ); ?> />
2100 <?php esc_html_e( 'Daily', 'wpvulnerability' ); ?>
2101 </label>
2102 <br/>
2103 <label>
2104 <input type="radio" name="wpvulnerability-config[period]" value="weekly" <?php checked( $wpvulnerability_settings['period'], 'weekly' ); ?> />
2105 <?php esc_html_e( 'Weekly', 'wpvulnerability' ); ?>
2106 </label>
2107 <div id="wpvulnerability_day_wrap">
2108 <br/>
2109 <label for="wpvulnerability_day"><?php esc_html_e( 'Day', 'wpvulnerability' ); ?></label>
2110 <select name="wpvulnerability-config[day]" id="wpvulnerability_day">
2111 <option value="monday" <?php selected( $wpvulnerability_settings['day'], 'monday' ); ?>><?php esc_html_e( 'Monday', 'wpvulnerability' ); ?></option>
2112 <option value="tuesday" <?php selected( $wpvulnerability_settings['day'], 'tuesday' ); ?>><?php esc_html_e( 'Tuesday', 'wpvulnerability' ); ?></option>
2113 <option value="wednesday" <?php selected( $wpvulnerability_settings['day'], 'wednesday' ); ?>><?php esc_html_e( 'Wednesday', 'wpvulnerability' ); ?></option>
2114 <option value="thursday" <?php selected( $wpvulnerability_settings['day'], 'thursday' ); ?>><?php esc_html_e( 'Thursday', 'wpvulnerability' ); ?></option>
2115 <option value="friday" <?php selected( $wpvulnerability_settings['day'], 'friday' ); ?>><?php esc_html_e( 'Friday', 'wpvulnerability' ); ?></option>
2116 <option value="saturday" <?php selected( $wpvulnerability_settings['day'], 'saturday' ); ?>><?php esc_html_e( 'Saturday', 'wpvulnerability' ); ?></option>
2117 <option value="sunday" <?php selected( $wpvulnerability_settings['day'], 'sunday' ); ?>><?php esc_html_e( 'Sunday', 'wpvulnerability' ); ?></option>
2118 </select>
2119 </div>
2120 <div id="wpvulnerability_time_wrap">
2121 <br/>
2122 <label for="wpvulnerability_hour"><?php esc_html_e( 'Hour', 'wpvulnerability' ); ?></label>
2123 <input type="number" min="0" max="23" name="wpvulnerability-config[hour]" id="wpvulnerability_hour" value="<?php echo esc_attr( (string) $wpvulnerability_settings['hour'] ); ?>" />
2124 <label for="wpvulnerability_minute"><?php esc_html_e( 'Minute', 'wpvulnerability' ); ?></label>
2125 <input type="number" min="0" max="59" name="wpvulnerability-config[minute]" id="wpvulnerability_minute" value="<?php echo esc_attr( (string) $wpvulnerability_settings['minute'] ); ?>" />
2126 </div>
2127 </div>
2128 <?php
2129 }
2130
2131 /**
2132 * Print where to send the notifications.
2133 *
2134 * @since 3.6.0
2135 *
2136 * @return void
2137 */
2138 function wpvulnerability_admin_notify_callback() {
2139 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
2140 if ( ! is_array( $wpvulnerability_settings ) ) {
2141 $wpvulnerability_settings = array();
2142 }
2143 $defaults = array(
2144 'email' => 'y',
2145 'slack' => 'n',
2146 'teams' => 'n',
2147 );
2148
2149 if ( ! isset( $wpvulnerability_settings['notify'] ) || ! is_array( $wpvulnerability_settings['notify'] ) ) {
2150 $wpvulnerability_settings['notify'] = $defaults;
2151 } else {
2152 $wpvulnerability_settings['notify'] = wp_parse_args( $wpvulnerability_settings['notify'], $defaults );
2153 }
2154
2155 $wpvulnerability_settings['notify'] = wpvulnerability_normalize_notify_settings( $wpvulnerability_settings['notify'] );
2156
2157 $email_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['email'] );
2158 $slack_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['slack'] );
2159 $teams_enabled = wpvulnerability_is_yes( $wpvulnerability_settings['notify']['teams'] );
2160
2161 ?>
2162 <div id="wpvulnerability_notify">
2163 <label>
2164 <input type="checkbox" name="wpvulnerability-config[notify][email]" value="y" <?php checked( $email_enabled ); ?> />
2165 <?php esc_html_e( 'Email', 'wpvulnerability' ); ?>
2166 </label>
2167 <br/>
2168 <label>
2169 <input type="checkbox" name="wpvulnerability-config[notify][slack]" value="y" <?php checked( $slack_enabled ); ?> />
2170 <?php esc_html_e( 'Slack', 'wpvulnerability' ); ?>
2171 </label>
2172 <br/>
2173 <label>
2174 <input type="checkbox" name="wpvulnerability-config[notify][teams]" value="y" <?php checked( $teams_enabled ); ?> />
2175 <?php esc_html_e( 'Microsoft Teams', 'wpvulnerability' ); ?>
2176 </label>
2177 </div>
2178 <?php
2179 }
2180
2181 /**
2182 * Print the Slack webhook input field.
2183 *
2184 * @since 3.6.0
2185 *
2186 * @return void
2187 */
2188 function wpvulnerability_admin_slack_callback() {
2189
2190 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
2191 if ( ! is_array( $wpvulnerability_settings ) ) {
2192 $wpvulnerability_settings = array();
2193 }
2194 $slack_webhook = isset( $wpvulnerability_settings['slack_webhook'] ) ? $wpvulnerability_settings['slack_webhook'] : '';
2195
2196 ?>
2197 <input class="regular-text" type="text" name="wpvulnerability-config[slack_webhook]" id="wpvulnerability_slack_webhook" placeholder="<?php echo esc_attr( 'https://hooks.slack.com/services/...' ); ?>" value="<?php echo esc_attr( is_scalar( $slack_webhook ) ? (string) $slack_webhook : '' ); ?>" />
2198 <?php
2199 }
2200
2201 /**
2202 * Print the Teams webhook input field.
2203 *
2204 * @since 3.6.0
2205 *
2206 * @return void
2207 */
2208 function wpvulnerability_admin_teams_callback() {
2209
2210 $wpvulnerability_settings = get_site_option( 'wpvulnerability-config', array() );
2211 if ( ! is_array( $wpvulnerability_settings ) ) {
2212 $wpvulnerability_settings = array();
2213 }
2214 $teams_webhook = isset( $wpvulnerability_settings['teams_webhook'] ) ? $wpvulnerability_settings['teams_webhook'] : '';
2215
2216 ?>
2217 <input class="regular-text" type="text" name="wpvulnerability-config[teams_webhook]" id="wpvulnerability_teams_webhook" placeholder="<?php echo esc_attr( 'https://outlook.office.com/webhook/...' ); ?>" value="<?php echo esc_attr( is_scalar( $teams_webhook ) ? (string) $teams_webhook : '' ); ?>" />
2218 <?php
2219 }
2220
2221 /**
2222 * Displays the WPVulnerability plugin analysis settings in the admin panel.
2223 *
2224 * This function retrieves the current WPVulnerability analysis settings and
2225 * ensures all necessary options are set. It then outputs a multiple-select
2226 * field allowing the user to select which components (core, plugins, themes,
2227 * php, apache, nginx) to analyze.
2228 *
2229 * @since 3.3.0
2230 *
2231 * @return void
2232 */
2233 function wpvulnerability_admin_analyze_callback() {
2234
2235 // Retrieve the WPVulnerability plugin settings.
2236 $wpvulnerability_analyze = get_site_option( 'wpvulnerability-analyze', array() );
2237 if ( ! is_array( $wpvulnerability_analyze ) ) {
2238 $wpvulnerability_analyze = array();
2239 }
2240 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
2241 $forced = array();
2242
2243 foreach ( $components as $component ) {
2244 if ( ! isset( $wpvulnerability_analyze[ $component ] ) ) {
2245 $wpvulnerability_analyze[ $component ] = 0;
2246 }
2247 $constant = 'WPVULNERABILITY_HIDE_' . strtoupper( (string) $component );
2248 $forced[ $component ] = defined( $constant ) && constant( $constant );
2249 if ( $forced[ $component ] ) {
2250 $wpvulnerability_analyze[ $component ] = 1;
2251 }
2252 }
2253 ?>
2254 <div id="wpvulnerability_analyze">
2255 <label>
2256 <input type="checkbox" name="wpvulnerability-analyze[core]" value="core" <?php checked( $wpvulnerability_analyze['core'] ); ?> <?php disabled( $forced['core'] ); ?> />
2257 <?php esc_html_e( 'Core', 'wpvulnerability' ); ?>
2258 </label>
2259 <br/>
2260 <label>
2261 <input type="checkbox" name="wpvulnerability-analyze[plugins]" value="plugins" <?php checked( $wpvulnerability_analyze['plugins'] ); ?> <?php disabled( $forced['plugins'] ); ?> />
2262 <?php esc_html_e( 'Plugins', 'wpvulnerability' ); ?>
2263 </label>
2264 <br/>
2265 <label>
2266 <input type="checkbox" name="wpvulnerability-analyze[themes]" value="themes" <?php checked( $wpvulnerability_analyze['themes'] ); ?> <?php disabled( $forced['themes'] ); ?> />
2267 <?php esc_html_e( 'Themes', 'wpvulnerability' ); ?>
2268 </label>
2269 <br/>
2270 <label>
2271 <input type="checkbox" name="wpvulnerability-analyze[php]" value="php" <?php checked( $wpvulnerability_analyze['php'] ); ?> <?php disabled( $forced['php'] ); ?> />
2272 <?php esc_html_e( 'PHP', 'wpvulnerability' ); ?>
2273 </label>
2274 <br/>
2275 <label>
2276 <input type="checkbox" name="wpvulnerability-analyze[apache]" value="apache" <?php checked( $wpvulnerability_analyze['apache'] ); ?> <?php disabled( $forced['apache'] ); ?> />
2277 <?php esc_html_e( 'Apache HTTPD', 'wpvulnerability' ); ?>
2278 </label>
2279 <br/>
2280 <label>
2281 <input type="checkbox" name="wpvulnerability-analyze[nginx]" value="nginx" <?php checked( $wpvulnerability_analyze['nginx'] ); ?> <?php disabled( $forced['nginx'] ); ?> />
2282 <?php esc_html_e( 'nginx', 'wpvulnerability' ); ?>
2283 </label>
2284 <br/>
2285 <label>
2286 <input type="checkbox" name="wpvulnerability-analyze[mariadb]" value="mariadb" <?php checked( $wpvulnerability_analyze['mariadb'] ); ?> <?php disabled( $forced['mariadb'] ); ?> />
2287 <?php esc_html_e( 'MariaDB', 'wpvulnerability' ); ?>
2288 </label>
2289 <br/>
2290 <label>
2291 <input type="checkbox" name="wpvulnerability-analyze[mysql]" value="mysql" <?php checked( $wpvulnerability_analyze['mysql'] ); ?> <?php disabled( $forced['mysql'] ); ?> />
2292 <?php esc_html_e( 'MySQL', 'wpvulnerability' ); ?>
2293 </label>
2294 <br/>
2295 <label>
2296 <input type="checkbox" name="wpvulnerability-analyze[imagemagick]" value="imagemagick" <?php checked( $wpvulnerability_analyze['imagemagick'] ); ?> <?php disabled( $forced['imagemagick'] ); ?> />
2297 <?php esc_html_e( 'ImageMagick', 'wpvulnerability' ); ?>
2298 </label>
2299 <br/>
2300 <label>
2301 <input type="checkbox" name="wpvulnerability-analyze[curl]" value="curl" <?php checked( $wpvulnerability_analyze['curl'] ); ?> <?php disabled( $forced['curl'] ); ?> />
2302 <?php esc_html_e( 'curl', 'wpvulnerability' ); ?>
2303 </label>
2304 <br/>
2305 <label>
2306 <input type="checkbox" name="wpvulnerability-analyze[memcached]" value="memcached" <?php checked( $wpvulnerability_analyze['memcached'] ); ?> <?php disabled( $forced['memcached'] ); ?> />
2307 <?php esc_html_e( 'memcached', 'wpvulnerability' ); ?>
2308 </label>
2309 <br/>
2310 <label>
2311 <input type="checkbox" name="wpvulnerability-analyze[redis]" value="redis" <?php checked( $wpvulnerability_analyze['redis'] ); ?> <?php disabled( $forced['redis'] ); ?> />
2312 <?php esc_html_e( 'Redis', 'wpvulnerability' ); ?>
2313 </label>
2314 <br/>
2315 <label>
2316 <input type="checkbox" name="wpvulnerability-analyze[sqlite]" value="sqlite" <?php checked( $wpvulnerability_analyze['sqlite'] ); ?> <?php disabled( $forced['sqlite'] ); ?> />
2317 <?php esc_html_e( 'SQLite', 'wpvulnerability' ); ?>
2318 </label>
2319 <p><a href="https://www.wpvulnerability.com/plugin/#force-hiding-checks" target="_blank"><small><i><?php esc_html_e( 'Read more about how to force the deactivation of an item.', 'wpvulnerability' ); ?></i></small></a></p>
2320 </div>
2321 <?php
2322 }
2323
2324 /**
2325 * Content for the Dashboard Widget
2326 *
2327 * @since 2.2.0
2328 *
2329 * @return void
2330 */
2331 function wpvulnerability_admin_dashboard_content() {
2332
2333 if ( ! wpvulnerability_capabilities() ) {
2334 return;
2335 }
2336
2337 // Get vulnerability counts for all components.
2338 $core_count = wpvulnerability_get_component_count( 'core' );
2339 $plugins_count = wpvulnerability_get_component_count( 'plugins' );
2340 $themes_count = wpvulnerability_get_component_count( 'themes' );
2341 $php_count = wpvulnerability_get_component_count( 'php' );
2342 $apache_count = wpvulnerability_get_component_count( 'apache' );
2343 $nginx_count = wpvulnerability_get_component_count( 'nginx' );
2344 $mariadb_count = wpvulnerability_get_component_count( 'mariadb' );
2345 $mysql_count = wpvulnerability_get_component_count( 'mysql' );
2346 $imagemagick_count = wpvulnerability_get_component_count( 'imagemagick' );
2347 $curl_count = wpvulnerability_get_component_count( 'curl' );
2348 $memcached_count = wpvulnerability_get_component_count( 'memcached' );
2349 $redis_count = wpvulnerability_get_component_count( 'redis' );
2350 $sqlite_count = wpvulnerability_get_component_count( 'sqlite' );
2351
2352 // Calculate total vulnerabilities (only for enabled components).
2353 $total_vulnerabilities = 0;
2354 if ( wpvulnerability_analyze_filter( 'core' ) ) {
2355 $total_vulnerabilities += $core_count;
2356 }
2357 if ( wpvulnerability_analyze_filter( 'plugins' ) ) {
2358 $total_vulnerabilities += $plugins_count;
2359 }
2360 if ( wpvulnerability_analyze_filter( 'themes' ) ) {
2361 $total_vulnerabilities += $themes_count;
2362 }
2363 if ( wpvulnerability_analyze_filter( 'php' ) ) {
2364 $total_vulnerabilities += $php_count;
2365 }
2366 if ( wpvulnerability_analyze_filter( 'apache' ) ) {
2367 $total_vulnerabilities += $apache_count;
2368 }
2369 if ( wpvulnerability_analyze_filter( 'nginx' ) ) {
2370 $total_vulnerabilities += $nginx_count;
2371 }
2372 if ( wpvulnerability_analyze_filter( 'mariadb' ) ) {
2373 $total_vulnerabilities += $mariadb_count;
2374 }
2375 if ( wpvulnerability_analyze_filter( 'mysql' ) ) {
2376 $total_vulnerabilities += $mysql_count;
2377 }
2378 if ( wpvulnerability_analyze_filter( 'imagemagick' ) ) {
2379 $total_vulnerabilities += $imagemagick_count;
2380 }
2381 if ( wpvulnerability_analyze_filter( 'curl' ) ) {
2382 $total_vulnerabilities += $curl_count;
2383 }
2384 if ( wpvulnerability_analyze_filter( 'memcached' ) ) {
2385 $total_vulnerabilities += $memcached_count;
2386 }
2387 if ( wpvulnerability_analyze_filter( 'redis' ) ) {
2388 $total_vulnerabilities += $redis_count;
2389 }
2390 if ( wpvulnerability_analyze_filter( 'sqlite' ) ) {
2391 $total_vulnerabilities += $sqlite_count;
2392 }
2393
2394 // Determine status badge.
2395 $status_class = 'wpvuln-status-secure';
2396 $status_text = '' . __( 'All Clear', 'wpvulnerability' );
2397 $status_icon = '';
2398
2399 if ( $total_vulnerabilities > 0 ) {
2400 if ( ( wpvulnerability_analyze_filter( 'core' ) && $core_count > 0 ) ||
2401 ( wpvulnerability_analyze_filter( 'php' ) && $php_count > 0 ) ||
2402 $total_vulnerabilities > 5 ) {
2403 $status_class = 'wpvuln-status-critical';
2404 $status_icon = '';
2405 /* translators: %d: number of vulnerabilities */
2406 $status_text = sprintf( _n( '%d Critical Issue Found', '%d Critical Issues Found', $total_vulnerabilities, 'wpvulnerability' ), $total_vulnerabilities );
2407 } else {
2408 $status_class = 'wpvuln-status-warning';
2409 $status_icon = '';
2410 /* translators: %d: number of vulnerabilities */
2411 $status_text = sprintf( _n( '%d Issue Found', '%d Issues Found', $total_vulnerabilities, 'wpvulnerability' ), $total_vulnerabilities );
2412 }
2413 }
2414
2415 // Get last check time.
2416 $raw_core_cache = get_site_option( 'wpvulnerability-core-cache', '' );
2417 $core_cache = json_decode( is_string( $raw_core_cache ) ? $raw_core_cache : '', true );
2418 $last_check_text = __( 'Never checked', 'wpvulnerability' );
2419 if ( $core_cache && is_numeric( $core_cache ) ) {
2420 $cache_hours = wpvulnerability_cache_hours();
2421 $last_check_time = $core_cache - ( $cache_hours * 3600 );
2422 $time_diff = time() - $last_check_time;
2423 if ( $time_diff < 3600 ) {
2424 $minutes_ago = (int) floor( $time_diff / 60 );
2425 /* translators: %d: number of minutes */
2426 $last_check_text = sprintf( _n( '%d minute ago', '%d minutes ago', $minutes_ago, 'wpvulnerability' ), $minutes_ago );
2427 } else {
2428 $hours_ago = (int) floor( $time_diff / 3600 );
2429 /* translators: %d: number of hours */
2430 $last_check_text = sprintf( _n( '%d hour ago', '%d hours ago', $hours_ago, 'wpvulnerability' ), $hours_ago );
2431 }
2432 }
2433
2434 // Status badge.
2435 echo '<div class="wpvuln-status-badge ' . esc_attr( $status_class ) . '">';
2436 echo esc_html( $status_text );
2437 echo '</div>';
2438
2439 // Meta information.
2440 echo '<div class="wpvuln-meta">';
2441 /* translators: %s: time since last check */
2442 echo esc_html( sprintf( __( 'Last checked: %s', 'wpvulnerability' ), $last_check_text ) );
2443 echo ' | <a href="' . esc_url( network_admin_url( 'settings.php?page=wpvulnerability-options&tab=tools' ) ) . '" class="wpvuln-refresh-btn">↻ ' . esc_html( __( 'Refresh Now', 'wpvulnerability' ) ) . '</a>';
2444 echo '</div>';
2445
2446 // If no vulnerabilities, show empty state.
2447 if ( 0 === $total_vulnerabilities ) {
2448 echo '<div class="wpvuln-empty-state">';
2449 echo '<div class="wpvuln-empty-state-icon">✓</div>';
2450 echo '<div class="wpvuln-empty-state-title">' . esc_html( __( 'No vulnerabilities detected', 'wpvulnerability' ) ) . '</div>';
2451 echo '<div class="wpvuln-empty-state-text">' . esc_html( __( 'Your site is up to date and secure', 'wpvulnerability' ) ) . '</div>';
2452 echo '</div>';
2453 } else {
2454 // WordPress Components section.
2455 echo '<div class="wpvuln-section-title">' . esc_html( __( 'WordPress Components', 'wpvulnerability' ) ) . '</div>';
2456
2457 // Core.
2458 if ( wpvulnerability_analyze_filter( 'core' ) ) {
2459 $badge_class = $core_count > 0 ? 'wpvuln-badge-critical' : 'wpvuln-badge-secure';
2460 $badge_icon = $core_count > 0 ? '' : '';
2461 echo '<div class="wpvuln-component">';
2462 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-wordpress.svg" width="16" height="16" alt="">';
2463 echo '<span class="wpvuln-component-name">' . esc_html( __( 'WordPress Core', 'wpvulnerability' ) ) . '</span>';
2464 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $core_count ) . '</span>';
2465 echo '</div>';
2466 }
2467
2468 // Plugins.
2469 if ( wpvulnerability_analyze_filter( 'plugins' ) ) {
2470 $badge_class = $plugins_count > 0 ? 'wpvuln-badge-critical' : 'wpvuln-badge-secure';
2471 $badge_icon = $plugins_count > 0 ? '' : '';
2472 echo '<div class="wpvuln-component">';
2473 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-plugin.svg" width="16" height="16" alt="">';
2474 echo '<span class="wpvuln-component-name">' . esc_html( __( 'Plugins', 'wpvulnerability' ) ) . '</span>';
2475 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $plugins_count ) . '</span>';
2476 echo '</div>';
2477 if ( $plugins_count > 0 ) {
2478 echo '<div class="wpvuln-plugin-list">';
2479 echo wpvulnerability_list_plugins(); // phpcs:ignore
2480 echo '</div>';
2481 }
2482 }
2483
2484 // Themes.
2485 if ( wpvulnerability_analyze_filter( 'themes' ) ) {
2486 $badge_class = $themes_count > 0 ? 'wpvuln-badge-critical' : 'wpvuln-badge-secure';
2487 $badge_icon = $themes_count > 0 ? '' : '';
2488 echo '<div class="wpvuln-component">';
2489 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-theme.svg" width="16" height="16" alt="">';
2490 echo '<span class="wpvuln-component-name">' . esc_html( __( 'Themes', 'wpvulnerability' ) ) . '</span>';
2491 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $themes_count ) . '</span>';
2492 echo '</div>';
2493 if ( $themes_count > 0 ) {
2494 echo '<div class="wpvuln-plugin-list">';
2495 echo wpvulnerability_list_themes(); // phpcs:ignore
2496 echo '</div>';
2497 }
2498 }
2499
2500 // Server Software section.
2501 $php_version = wpvulnerability_detect_php();
2502 $webserver = wpvulnerability_detect_webserver();
2503 $sqlserver = wpvulnerability_detect_sqlserver();
2504 $show_server_section = false;
2505
2506 // Check if any server software is detected.
2507 if ( ( wpvulnerability_analyze_filter( 'php' ) && $php_version ) ||
2508 ( isset( $webserver['id'] ) && isset( $webserver['version'] ) ) ||
2509 ( isset( $sqlserver['id'] ) && isset( $sqlserver['version'] ) ) ||
2510 wpvulnerability_get_software_version( 'imagemagick' ) ||
2511 wpvulnerability_get_software_version( 'curl' ) ||
2512 wpvulnerability_get_software_version( 'memcached' ) ||
2513 wpvulnerability_get_software_version( 'redis' ) ||
2514 wpvulnerability_get_software_version( 'sqlite' ) ) {
2515 $show_server_section = true;
2516 }
2517
2518 if ( $show_server_section ) {
2519 echo '<div class="wpvuln-section-title">' . esc_html( __( 'Server Software', 'wpvulnerability' ) ) . '</div>';
2520 echo '<div class="wpvuln-grid">';
2521
2522 // PHP.
2523 if ( wpvulnerability_analyze_filter( 'php' ) && $php_version ) {
2524 $badge_class = $php_count > 0 ? 'wpvuln-badge-critical' : 'wpvuln-badge-secure';
2525 $badge_icon = $php_count > 0 ? '' : '';
2526 echo '<div class="wpvuln-component">';
2527 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-php.svg" width="16" height="16" alt="">';
2528 echo '<span class="wpvuln-component-name">PHP ' . esc_html( $php_version ) . '</span>';
2529 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $php_count ) . '</span>';
2530 echo wp_kses(
2531 wpvulnerability_eol_badge_html( 'php' ),
2532 array(
2533 'span' => array(
2534 'class' => array(),
2535 'title' => array(),
2536 ),
2537 )
2538 );
2539 echo '</div>';
2540 }
2541
2542 // Web server (Apache/nginx).
2543 if ( isset( $webserver['id'] ) && isset( $webserver['version'] ) ) {
2544 if ( 'apache' === $webserver['id'] && wpvulnerability_analyze_filter( 'apache' ) ) {
2545 $badge_class = $apache_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2546 $badge_icon = $apache_count > 0 ? '' : '';
2547 echo '<div class="wpvuln-component">';
2548 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-apache.svg" width="16" height="16" alt="">';
2549 echo '<span class="wpvuln-component-name">Apache ' . esc_html( $webserver['version'] ) . '</span>';
2550 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $apache_count ) . '</span>';
2551 echo wp_kses(
2552 wpvulnerability_eol_badge_html( 'apache' ),
2553 array(
2554 'span' => array(
2555 'class' => array(),
2556 'title' => array(),
2557 ),
2558 )
2559 );
2560 echo '</div>';
2561 } elseif ( 'nginx' === $webserver['id'] && wpvulnerability_analyze_filter( 'nginx' ) ) {
2562 $badge_class = $nginx_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2563 $badge_icon = $nginx_count > 0 ? '' : '';
2564 echo '<div class="wpvuln-component">';
2565 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-nginx.svg" width="16" height="16" alt="">';
2566 echo '<span class="wpvuln-component-name">nginx ' . esc_html( $webserver['version'] ) . '</span>';
2567 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $nginx_count ) . '</span>';
2568 echo wp_kses(
2569 wpvulnerability_eol_badge_html( 'nginx' ),
2570 array(
2571 'span' => array(
2572 'class' => array(),
2573 'title' => array(),
2574 ),
2575 )
2576 );
2577 echo '</div>';
2578 }
2579 }
2580
2581 // Database (MariaDB/MySQL).
2582 if ( isset( $sqlserver['id'] ) && isset( $sqlserver['version'] ) ) {
2583 if ( 'mariadb' === $sqlserver['id'] && wpvulnerability_analyze_filter( 'mariadb' ) ) {
2584 $badge_class = $mariadb_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2585 $badge_icon = $mariadb_count > 0 ? '' : '';
2586 echo '<div class="wpvuln-component">';
2587 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-mariadb.svg" width="16" height="16" alt="">';
2588 echo '<span class="wpvuln-component-name">MariaDB ' . esc_html( $sqlserver['version'] ) . '</span>';
2589 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $mariadb_count ) . '</span>';
2590 echo wp_kses(
2591 wpvulnerability_eol_badge_html( 'mariadb' ),
2592 array(
2593 'span' => array(
2594 'class' => array(),
2595 'title' => array(),
2596 ),
2597 )
2598 );
2599 echo '</div>';
2600 } elseif ( 'mysql' === $sqlserver['id'] && wpvulnerability_analyze_filter( 'mysql' ) ) {
2601 $badge_class = $mysql_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2602 $badge_icon = $mysql_count > 0 ? '' : '';
2603 echo '<div class="wpvuln-component">';
2604 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-mysql.svg" width="16" height="16" alt="">';
2605 echo '<span class="wpvuln-component-name">MySQL ' . esc_html( $sqlserver['version'] ) . '</span>';
2606 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $mysql_count ) . '</span>';
2607 echo wp_kses(
2608 wpvulnerability_eol_badge_html( 'mysql' ),
2609 array(
2610 'span' => array(
2611 'class' => array(),
2612 'title' => array(),
2613 ),
2614 )
2615 );
2616 echo '</div>';
2617 }
2618 }
2619
2620 // ImageMagick.
2621 $imagemagick_version = wpvulnerability_get_software_version( 'imagemagick' );
2622 if ( $imagemagick_version && wpvulnerability_analyze_filter( 'imagemagick' ) ) {
2623 $badge_class = $imagemagick_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2624 $badge_icon = $imagemagick_count > 0 ? '' : '';
2625 echo '<div class="wpvuln-component">';
2626 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-imagemagick.svg" width="16" height="16" alt="">';
2627 echo '<span class="wpvuln-component-name">ImageMagick ' . esc_html( $imagemagick_version ) . '</span>';
2628 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $imagemagick_count ) . '</span>';
2629 echo wp_kses(
2630 wpvulnerability_eol_badge_html( 'imagemagick' ),
2631 array(
2632 'span' => array(
2633 'class' => array(),
2634 'title' => array(),
2635 ),
2636 )
2637 );
2638 echo '</div>';
2639 }
2640
2641 // curl.
2642 $curl_version = wpvulnerability_get_software_version( 'curl' );
2643 if ( $curl_version && wpvulnerability_analyze_filter( 'curl' ) ) {
2644 $badge_class = $curl_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2645 $badge_icon = $curl_count > 0 ? '' : '';
2646 echo '<div class="wpvuln-component">';
2647 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-curl.svg" width="16" height="16" alt="">';
2648 echo '<span class="wpvuln-component-name">curl ' . esc_html( $curl_version ) . '</span>';
2649 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $curl_count ) . '</span>';
2650 echo wp_kses(
2651 wpvulnerability_eol_badge_html( 'curl' ),
2652 array(
2653 'span' => array(
2654 'class' => array(),
2655 'title' => array(),
2656 ),
2657 )
2658 );
2659 echo '</div>';
2660 }
2661
2662 // memcached.
2663 $memcached_version = wpvulnerability_get_software_version( 'memcached' );
2664 if ( $memcached_version && wpvulnerability_analyze_filter( 'memcached' ) ) {
2665 $badge_class = $memcached_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2666 $badge_icon = $memcached_count > 0 ? '' : '';
2667 echo '<div class="wpvuln-component">';
2668 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-memcached.svg" width="16" height="16" alt="">';
2669 echo '<span class="wpvuln-component-name">memcached ' . esc_html( $memcached_version ) . '</span>';
2670 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $memcached_count ) . '</span>';
2671 echo wp_kses(
2672 wpvulnerability_eol_badge_html( 'memcached' ),
2673 array(
2674 'span' => array(
2675 'class' => array(),
2676 'title' => array(),
2677 ),
2678 )
2679 );
2680 echo '</div>';
2681 }
2682
2683 // Redis.
2684 $redis_version = wpvulnerability_get_software_version( 'redis' );
2685 if ( $redis_version && wpvulnerability_analyze_filter( 'redis' ) ) {
2686 $badge_class = $redis_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2687 $badge_icon = $redis_count > 0 ? '' : '';
2688 echo '<div class="wpvuln-component">';
2689 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-redis.svg" width="16" height="16" alt="">';
2690 echo '<span class="wpvuln-component-name">Redis ' . esc_html( $redis_version ) . '</span>';
2691 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $redis_count ) . '</span>';
2692 echo wp_kses(
2693 wpvulnerability_eol_badge_html( 'redis' ),
2694 array(
2695 'span' => array(
2696 'class' => array(),
2697 'title' => array(),
2698 ),
2699 )
2700 );
2701 echo '</div>';
2702 }
2703
2704 // SQLite.
2705 $sqlite_version = wpvulnerability_get_software_version( 'sqlite' );
2706 if ( $sqlite_version && wpvulnerability_analyze_filter( 'sqlite' ) ) {
2707 $badge_class = $sqlite_count > 0 ? 'wpvuln-badge-warning' : 'wpvuln-badge-secure';
2708 $badge_icon = $sqlite_count > 0 ? '' : '';
2709 echo '<div class="wpvuln-component">';
2710 echo '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon-sqlite.svg" width="16" height="16" alt="">';
2711 echo '<span class="wpvuln-component-name">SQLite ' . esc_html( $sqlite_version ) . '</span>';
2712 echo '<span class="wpvuln-badge ' . esc_attr( $badge_class ) . '">' . esc_html( $badge_icon ) . ' ' . esc_html( (string) $sqlite_count ) . '</span>';
2713 echo wp_kses(
2714 wpvulnerability_eol_badge_html( 'sqlite' ),
2715 array(
2716 'span' => array(
2717 'class' => array(),
2718 'title' => array(),
2719 ),
2720 )
2721 );
2722 echo '</div>';
2723 }
2724
2725 echo '</div>';
2726 }
2727 }
2728
2729 // Footer links.
2730 echo '<div class="wpvuln-footer">';
2731 if ( version_compare( get_bloginfo( 'version' ), '5.2', '>=' ) ) {
2732 echo '<a href="' . esc_url( get_admin_url( get_main_site_id(), 'site-health.php' ) ) . '">' . esc_html( __( 'Site Health', 'wpvulnerability' ) ) . '</a> | ';
2733 }
2734 echo '<a href="' . esc_url( network_admin_url( 'settings.php?page=wpvulnerability-options' ) ) . '">' . esc_html( __( 'Settings', 'wpvulnerability' ) ) . '</a>';
2735 echo '</div>';
2736 }
2737
2738 /**
2739 * Created a widget in the WordPress dashboard with vulnerability info.
2740 *
2741 * @since 2.2.0
2742 *
2743 * @return void
2744 */
2745 function wpvulnerability_admin_dashboard() {
2746
2747 if ( wpvulnerability_capabilities() ) {
2748 wp_add_dashboard_widget(
2749 'wpvulnerability',
2750 __( 'WPVulnerability Status', 'wpvulnerability' ),
2751 'wpvulnerability_admin_dashboard_content',
2752 null,
2753 null,
2754 'side',
2755 'high'
2756 );
2757 }
2758 }
2759 add_action( 'wp_network_dashboard_setup', 'wpvulnerability_admin_dashboard' );
2760
2761 /**
2762 * Strictly sanitizes the main configuration (emails and periods).
2763 *
2764 * @since 2.0.0
2765 *
2766 * @param array<string, mixed> $input Input values.
2767 * @return array<string, mixed> Sanitized values.
2768 */
2769 function wpvulnerability_sanitize_config( $input ) {
2770 $defaults = array(
2771 'emails' => null,
2772 'period' => 'weekly',
2773 'day' => 'monday',
2774 'hour' => 0,
2775 'minute' => 0,
2776 'cache' => 12,
2777 'log_retention' => 0,
2778 'notify' => array(
2779 'email' => 'n',
2780 'slack' => 'n',
2781 'teams' => 'n',
2782 ),
2783 'slack_webhook' => '',
2784 'teams_webhook' => '',
2785 );
2786
2787 $current_values = get_site_option( 'wpvulnerability-config', array() );
2788 if ( ! is_array( $current_values ) ) {
2789 $current_values = array();
2790 }
2791
2792 $sanitized = $current_values;
2793
2794 // Emails (comma-separated list).
2795 if ( isset( $input['emails'] ) ) {
2796 $emails_raw = explode( ',', is_scalar( $input['emails'] ) ? (string) $input['emails'] : '' );
2797 $sanitized_emails = array();
2798
2799 foreach ( $emails_raw as $email ) {
2800 $email = sanitize_email( trim( (string) $email ) );
2801 if ( is_email( $email ) ) {
2802 $sanitized_emails[] = $email;
2803 }
2804 }
2805
2806 $sanitized['emails'] = count( $sanitized_emails ) ? implode( ',', $sanitized_emails ) : null;
2807 }
2808
2809 // Period (daily, weekly, never).
2810 $allowed_periods = array( 'daily', 'weekly', 'never' );
2811 if ( isset( $input['period'] ) && in_array( $input['period'], $allowed_periods, true ) ) {
2812 $sanitized['period'] = $input['period'];
2813 }
2814
2815 // Day of week for weekly schedule.
2816 if ( isset( $input['day'] ) ) {
2817 $day = strtolower( is_scalar( $input['day'] ) ? (string) $input['day'] : '' );
2818 $valid = array( 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' );
2819 $sanitized['day'] = in_array( $day, $valid, true ) ? $day : 'monday';
2820 }
2821
2822 // Time (hour and minute).
2823 if ( isset( $input['hour'] ) ) {
2824 $hour = is_scalar( $input['hour'] ) ? (int) $input['hour'] : 0;
2825 $sanitized['hour'] = max( 0, min( 23, $hour ) );
2826 }
2827 if ( isset( $input['minute'] ) ) {
2828 $minute = is_scalar( $input['minute'] ) ? (int) $input['minute'] : 0;
2829 $sanitized['minute'] = max( 0, min( 59, $minute ) );
2830 }
2831
2832 // Cache expiration.
2833 if ( isset( $input['cache'] ) ) {
2834 $cache = is_scalar( $input['cache'] ) ? (int) $input['cache'] : 0;
2835 $sanitized['cache'] = in_array( $cache, array( 1, 6, 12, 24 ), true ) ? $cache : 12;
2836 }
2837 if ( isset( $input['log_retention'] ) ) {
2838 $retention = is_scalar( $input['log_retention'] ) ? (int) $input['log_retention'] : 0;
2839 if ( in_array( $retention, wpvulnerability_get_log_retention_values(), true ) ) {
2840 $sanitized['log_retention'] = $retention;
2841 }
2842 }
2843
2844 // Notification methods.
2845 if ( isset( $input['notify'] ) && is_array( $input['notify'] ) ) {
2846 $notify_raw = (array) wp_unslash( $input['notify'] );
2847 $notify_input = array();
2848 foreach ( $notify_raw as $k => $v ) {
2849 $notify_input[ (string) $k ] = sanitize_text_field( is_scalar( $v ) ? (string) $v : '' );
2850 }
2851 $sanitized['notify'] = wpvulnerability_normalize_notify_settings( $notify_input );
2852 }
2853
2854 // Webhooks.
2855 if ( isset( $input['slack_webhook'] ) ) {
2856 $slack_url = trim( is_scalar( $input['slack_webhook'] ) ? (string) $input['slack_webhook'] : '' );
2857 if ( '' !== $slack_url ) {
2858 $validated_slack = wpvulnerability_validate_webhook_url(
2859 $slack_url,
2860 array( 'hooks.slack.com' )
2861 );
2862 if ( '' === $validated_slack ) {
2863 add_settings_error(
2864 'wpvulnerability-config',
2865 'invalid-slack-webhook',
2866 __( 'Invalid Slack webhook URL. Must be a valid HTTPS URL from hooks.slack.com', 'wpvulnerability' ),
2867 'error'
2868 );
2869 $sanitized['slack_webhook'] = '';
2870 } else {
2871 $sanitized['slack_webhook'] = $validated_slack;
2872 }
2873 } else {
2874 $sanitized['slack_webhook'] = '';
2875 }
2876 }
2877
2878 if ( isset( $input['teams_webhook'] ) ) {
2879 $teams_url = trim( is_scalar( $input['teams_webhook'] ) ? (string) $input['teams_webhook'] : '' );
2880 if ( '' !== $teams_url ) {
2881 $validated_teams = wpvulnerability_validate_webhook_url(
2882 $teams_url,
2883 array( 'office.com', 'office365.com', 'api.hooks.microsoft.com' )
2884 );
2885 if ( '' === $validated_teams ) {
2886 add_settings_error(
2887 'wpvulnerability-config',
2888 'invalid-teams-webhook',
2889 __( 'Invalid Microsoft Teams webhook URL. Must be a valid HTTPS URL from office.com, office365.com, or api.hooks.microsoft.com', 'wpvulnerability' ),
2890 'error'
2891 );
2892 $sanitized['teams_webhook'] = '';
2893 } else {
2894 $sanitized['teams_webhook'] = $validated_teams;
2895 }
2896 } else {
2897 $sanitized['teams_webhook'] = '';
2898 }
2899 }
2900
2901 if ( isset( $input['discord_webhook'] ) ) {
2902 $discord_url = trim( is_scalar( $input['discord_webhook'] ) ? (string) $input['discord_webhook'] : '' );
2903 if ( '' !== $discord_url ) {
2904 $validated_discord = wpvulnerability_validate_webhook_url(
2905 $discord_url,
2906 array( 'discord.com', 'discordapp.com' )
2907 );
2908 if ( '' === $validated_discord ) {
2909 add_settings_error(
2910 'wpvulnerability-config',
2911 'invalid-discord-webhook',
2912 __( 'Invalid Discord webhook URL. Must be a valid HTTPS URL from discord.com or discordapp.com', 'wpvulnerability' ),
2913 'error'
2914 );
2915 $sanitized['discord_webhook'] = '';
2916 } else {
2917 $sanitized['discord_webhook'] = $validated_discord;
2918 }
2919 } else {
2920 $sanitized['discord_webhook'] = '';
2921 }
2922 }
2923
2924 if ( isset( $input['telegram_bot_token'] ) ) {
2925 $telegram_bot_token = sanitize_text_field( trim( is_scalar( $input['telegram_bot_token'] ) ? (string) $input['telegram_bot_token'] : '' ) );
2926 if ( '' !== $telegram_bot_token ) {
2927 if ( ! preg_match( '/^\d+:[A-Za-z0-9_-]+$/', $telegram_bot_token ) ) {
2928 add_settings_error(
2929 'wpvulnerability-config',
2930 'invalid-telegram-token',
2931 __( 'Invalid Telegram bot token format. Must be like: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', 'wpvulnerability' ),
2932 'error'
2933 );
2934 $sanitized['telegram_bot_token'] = '';
2935 } else {
2936 $sanitized['telegram_bot_token'] = $telegram_bot_token;
2937 }
2938 } else {
2939 $sanitized['telegram_bot_token'] = '';
2940 }
2941 }
2942
2943 if ( isset( $input['telegram_chat_id'] ) ) {
2944 $sanitized['telegram_chat_id'] = sanitize_text_field( trim( is_scalar( $input['telegram_chat_id'] ) ? (string) $input['telegram_chat_id'] : '' ) );
2945 }
2946
2947 $sanitized = wp_parse_args( $sanitized, $defaults );
2948
2949 // Schedule notification based on sanitized values.
2950 wpvulnerability_schedule_notification_event( $sanitized );
2951
2952 return $sanitized;
2953 }
2954
2955 /**
2956 * Strictly sanitizes the analysis options (booleans).
2957 *
2958 * @since 3.3.0
2959 *
2960 * @param array<string, mixed> $input Input values.
2961 * @return array<string, bool> Sanitized values.
2962 */
2963 function wpvulnerability_sanitize_analyze( $input ) {
2964 $components = array(
2965 'core',
2966 'plugins',
2967 'themes',
2968 'php',
2969 'apache',
2970 'nginx',
2971 'mariadb',
2972 'mysql',
2973 'imagemagick',
2974 'curl',
2975 'memcached',
2976 'redis',
2977 'sqlite',
2978 );
2979
2980 $sanitized = array();
2981
2982 foreach ( $components as $component ) {
2983 $sanitized[ $component ] = isset( $input[ $component ] ) ? (bool) $input[ $component ] : false;
2984
2985 $constant = 'WPVULNERABILITY_HIDE_' . strtoupper( (string) $component );
2986 if ( defined( $constant ) && constant( $constant ) ) {
2987 $sanitized[ $component ] = true;
2988 }
2989 }
2990
2991 return $sanitized;
2992 }
2993
2994 /**
2995 * Initializes the WP-Admin settings page for the WP Vulnerability plugin.
2996 *
2997 * @since 2.0.0
2998 *
2999 * @return void
3000 */
3001 function wpvulnerability_admin_init() {
3002
3003 // Register the plugin settings to be saved in the database.
3004 register_setting(
3005 'admin_wpvulnerability_settings',
3006 'wpvulnerability-config',
3007 array(
3008 'sanitize_callback' => 'wpvulnerability_sanitize_config',
3009 'default' => array(),
3010 'show_in_rest' => false,
3011 'type' => 'array',
3012 )
3013 );
3014
3015 // Add a section to the settings page.
3016 add_settings_section(
3017 'admin_wpvulnerability_settings',
3018 __( 'Receive vulnerability notifications', 'wpvulnerability' ),
3019 'wpvulnerability_admin_section_notifications',
3020 'wpvulnerability-config'
3021 );
3022
3023 // Add a field to the settings page for the cache expiration time.
3024 add_settings_field(
3025 'wpvulnerability_cache',
3026 __( 'Cache expiration time', 'wpvulnerability' ),
3027 'wpvulnerability_admin_cache_callback',
3028 'wpvulnerability-config',
3029 'admin_wpvulnerability_settings'
3030 );
3031
3032 // Add a field to the settings page for the notification period.
3033 add_settings_field(
3034 'wpvulnerability_period',
3035 __( 'How often you want to receive notifications', 'wpvulnerability' ),
3036 'wpvulnerability_admin_period_callback',
3037 'wpvulnerability-config',
3038 'admin_wpvulnerability_settings'
3039 );
3040
3041 // Add a field for notification methods.
3042 add_settings_field(
3043 'wpvulnerability_notify',
3044 __( 'Where do you want to receive notifications?', 'wpvulnerability' ),
3045 'wpvulnerability_admin_notify_callback',
3046 'wpvulnerability-config',
3047 'admin_wpvulnerability_settings'
3048 );
3049
3050 // Add a field to the settings page for the email addresses.
3051 add_settings_field(
3052 'wpvulnerability_emails',
3053 __( 'Email addresses to notify (separated by commas)', 'wpvulnerability' ),
3054 'wpvulnerability_admin_emails_callback',
3055 'wpvulnerability-config',
3056 'admin_wpvulnerability_settings'
3057 );
3058
3059 // Add a field for the Slack webhook.
3060 add_settings_field(
3061 'wpvulnerability_slack_webhook',
3062 __( 'Slack webhook URL', 'wpvulnerability' ),
3063 'wpvulnerability_admin_slack_callback',
3064 'wpvulnerability-config',
3065 'admin_wpvulnerability_settings'
3066 );
3067
3068 // Add a field for the Teams webhook.
3069 add_settings_field(
3070 'wpvulnerability_teams_webhook',
3071 __( 'Teams webhook URL', 'wpvulnerability' ),
3072 'wpvulnerability_admin_teams_callback',
3073 'wpvulnerability-config',
3074 'admin_wpvulnerability_settings'
3075 );
3076
3077 // Register the plugin settings to be saved in the database.
3078 register_setting(
3079 'admin_wpvulnerability_analyze',
3080 'wpvulnerability-analyze',
3081 array(
3082 'sanitize_callback' => 'wpvulnerability_sanitize_analyze',
3083 'default' => array(),
3084 'show_in_rest' => false,
3085 'type' => 'array',
3086 )
3087 );
3088
3089 // Add a section to the settings page.
3090 add_settings_section(
3091 'admin_wpvulnerability_analyze',
3092 __( 'Vulnerabilities to hide', 'wpvulnerability' ),
3093 'wpvulnerability_admin_section_analyze',
3094 'wpvulnerability-analyze'
3095 );
3096
3097 // Add a field to the settings page for analyzing components.
3098 add_settings_field(
3099 'wpvulnerability_analyze',
3100 __( 'What do you want to hide?', 'wpvulnerability' ),
3101 'wpvulnerability_admin_analyze_callback',
3102 'wpvulnerability-analyze',
3103 'admin_wpvulnerability_analyze'
3104 );
3105 }
3106 add_action( 'admin_init', 'wpvulnerability_admin_init' );
3107
3108 /**
3109 * Outputs the Security tab contents for the multisite network admin.
3110 *
3111 * Reuses the single-site security display functions since the functionality
3112 * is identical for both single-site and multisite contexts.
3113 *
3114 * @since 4.3.0
3115 *
3116 * @return void
3117 */
3118 function wpvulnerability_render_network_admin_tab_security() {
3119 ?>
3120 <section class="section wpvulnerability-security-panel">
3121
3122 <?php wpvulnerability_display_security_status(); ?>
3123 <?php wpvulnerability_display_detection_methods(); ?>
3124 <?php wpvulnerability_display_security_logs(); ?>
3125 </section>
3126 <?php
3127 }
3128
3129 /**
3130 * Displays the security status section.
3131 *
3132 * Shows current security mode, shell_exec availability, and logging status.
3133 *
3134 * @since 4.3.0
3135 *
3136 * @return void
3137 */
3138 function wpvulnerability_display_security_status() {
3139 $security_mode = wpvulnerability_get_security_mode();
3140 $shell_exec_enabled = wpvulnerability_can_shell_exec();
3141 $log_retention = wpvulnerability_log_retention_days();
3142 $total_logs = wpvulnerability_count_shell_exec_logs();
3143 $last_log = null;
3144
3145 $recent_logs = wpvulnerability_get_shell_exec_logs( 1, 1 );
3146 if ( ! empty( $recent_logs ) ) {
3147 $last_log = $recent_logs[0];
3148 }
3149
3150 $mode_labels = array(
3151 'standard' => __( 'Standard (Hybrid Detection)', 'wpvulnerability' ),
3152 'strict' => __( 'Strict (Extensions Only)', 'wpvulnerability' ),
3153 'disabled' => __( 'Disabled (No Detection)', 'wpvulnerability' ),
3154 );
3155
3156 $mode_label = isset( $mode_labels[ $security_mode ] ) ? $mode_labels[ $security_mode ] : $security_mode;
3157 ?>
3158 <div class="wpvulnerability-security-section">
3159 <h3><?php esc_html_e( 'Security Status', 'wpvulnerability' ); ?></h3>
3160
3161 <div class="wpvulnerability-status-grid">
3162 <div class="wpvulnerability-status-item">
3163 <strong><?php esc_html_e( 'Security Mode', 'wpvulnerability' ); ?></strong>
3164 <span><?php echo esc_html( $mode_label ); ?></span>
3165 </div>
3166
3167 <div class="wpvulnerability-status-item">
3168 <strong><?php esc_html_e( 'Shell Execution', 'wpvulnerability' ); ?></strong>
3169 <?php if ( $shell_exec_enabled ) : ?>
3170 <span class="wpvulnerability-status-badge success"><?php esc_html_e( 'Enabled', 'wpvulnerability' ); ?></span>
3171 <?php else : ?>
3172 <span class="wpvulnerability-status-badge disabled"><?php esc_html_e( 'Disabled', 'wpvulnerability' ); ?></span>
3173 <?php endif; ?>
3174 </div>
3175
3176 <div class="wpvulnerability-status-item">
3177 <strong><?php esc_html_e( 'Audit Logging', 'wpvulnerability' ); ?></strong>
3178 <?php if ( $log_retention > 0 ) : ?>
3179 <span class="wpvulnerability-status-badge success">
3180 <?php
3181 /* translators: %d: Number of days. */
3182 echo esc_html( sprintf( __( 'Enabled (%d days)', 'wpvulnerability' ), $log_retention ) );
3183 ?>
3184 </span>
3185 <?php else : ?>
3186 <span class="wpvulnerability-status-badge disabled"><?php esc_html_e( 'Disabled', 'wpvulnerability' ); ?></span>
3187 <?php endif; ?>
3188 </div>
3189
3190 <div class="wpvulnerability-status-item">
3191 <strong><?php esc_html_e( 'Total Log Entries', 'wpvulnerability' ); ?></strong>
3192 <span><?php echo esc_html( number_format_i18n( $total_logs ) ); ?></span>
3193 </div>
3194 </div>
3195
3196 <?php if ( $last_log ) : ?>
3197 <div class="wpvulnerability-status-item" style="margin-top: 10px;">
3198 <strong><?php esc_html_e( 'Last Shell Execution', 'wpvulnerability' ); ?></strong>
3199 <span>
3200 <?php
3201 echo esc_html(
3202 sprintf(
3203 /* translators: %s: time ago */
3204 __( '%s ago', 'wpvulnerability' ),
3205 human_time_diff( absint( strtotime( $last_log->post_date ) ), time() )
3206 )
3207 );
3208 ?>
3209 </span>
3210 </div>
3211 <?php endif; ?>
3212
3213 <div class="wpvulnerability-info-box">
3214 <p>
3215 <?php
3216 esc_html_e( 'Configure security settings via wp-config.php constants: WPVULNERABILITY_DISABLE_SHELL_EXEC, WPVULNERABILITY_SECURITY_MODE, WPVULNERABILITY_SHELL_EXEC_WHITELIST.', 'wpvulnerability' );
3217 ?>
3218 <br>
3219 <?php
3220 printf(
3221 /* translators: %s: URL to security documentation */
3222 wp_kses_post( __( 'See <a href="%s" target="_blank" rel="noopener noreferrer">security configuration documentation</a> for details.', 'wpvulnerability' ) ),
3223 esc_url( 'https://www.wpvulnerability.com/plugin/#security-configuration' )
3224 );
3225 ?>
3226 </p>
3227 </div>
3228 </div>
3229 <?php
3230 }
3231
3232 /**
3233 * Displays the detection methods section.
3234 *
3235 * Shows which detection method was used for each software component
3236 * with reliability scoring.
3237 *
3238 * @since 4.3.0
3239 *
3240 * @return void
3241 */
3242 function wpvulnerability_display_detection_methods() {
3243 $components = array(
3244 'imagemagick' => __( 'ImageMagick', 'wpvulnerability' ),
3245 'redis' => __( 'Redis', 'wpvulnerability' ),
3246 'memcached' => __( 'Memcached', 'wpvulnerability' ),
3247 'sqlite' => __( 'SQLite', 'wpvulnerability' ),
3248 );
3249
3250 $detections = array();
3251
3252 foreach ( array_keys( $components ) as $component ) {
3253 $detection = null;
3254
3255 // Do not run detection for components hidden via the analysis settings or
3256 // a WPVULNERABILITY_HIDE_* constant: they must not trigger shell_exec.
3257 if ( ! wpvulnerability_analyze_filter( $component ) ) {
3258 continue;
3259 }
3260
3261 switch ( $component ) {
3262 case 'imagemagick':
3263 $detection = wpvulnerability_detect_imagemagick();
3264 break;
3265 case 'redis':
3266 $detection = wpvulnerability_detect_redis();
3267 break;
3268 case 'memcached':
3269 $detection = wpvulnerability_detect_memcached();
3270 break;
3271 case 'sqlite':
3272 $detection = wpvulnerability_detect_sqlite();
3273 break;
3274 }
3275
3276 $detections[ $component ] = $detection;
3277 }
3278
3279 $method_labels = array(
3280 'imagick_extension' => __( 'Imagick Extension', 'wpvulnerability' ),
3281 'redis_extension' => __( 'Redis Extension', 'wpvulnerability' ),
3282 'memcached_extension' => __( 'Memcached Extension', 'wpvulnerability' ),
3283 'memcache_extension' => __( 'Memcache Extension', 'wpvulnerability' ),
3284 'sqlite3_extension' => __( 'SQLite3 Extension', 'wpvulnerability' ),
3285 'pdo_sqlite' => __( 'PDO SQLite', 'wpvulnerability' ),
3286 'shell_exec' => __( 'Shell Command', 'wpvulnerability' ),
3287 'binary_exists' => __( 'Binary Check', 'wpvulnerability' ),
3288 'none' => __( 'Not Detected', 'wpvulnerability' ),
3289 );
3290 ?>
3291 <div class="wpvulnerability-security-section">
3292 <h3><?php esc_html_e( 'Software Detection Methods', 'wpvulnerability' ); ?></h3>
3293 <p><?php esc_html_e( 'Shows which detection method was used for each software component and the reliability score.', 'wpvulnerability' ); ?></p>
3294
3295 <table class="wpvulnerability-detection-table">
3296 <thead>
3297 <tr>
3298 <th><?php esc_html_e( 'Component', 'wpvulnerability' ); ?></th>
3299 <th><?php esc_html_e( 'Version', 'wpvulnerability' ); ?></th>
3300 <th><?php esc_html_e( 'Detection Method', 'wpvulnerability' ); ?></th>
3301 <th><?php esc_html_e( 'Reliability', 'wpvulnerability' ); ?></th>
3302 </tr>
3303 </thead>
3304 <tbody>
3305 <?php foreach ( $detections as $component => $detection ) : ?>
3306 <tr>
3307 <td><strong><?php echo esc_html( $components[ $component ] ); ?></strong></td>
3308 <td>
3309 <?php
3310 if ( ! empty( $detection['version'] ) && 'unknown' !== $detection['version'] ) {
3311 echo esc_html( $detection['version'] );
3312 } else {
3313 echo '<span style="color: #646970;">' . esc_html__( 'Not detected', 'wpvulnerability' ) . '</span>';
3314 }
3315 ?>
3316 </td>
3317 <td>
3318 <?php
3319 $method = isset( $method_labels[ $detection['method'] ] ) ? $method_labels[ $detection['method'] ] : $detection['method'];
3320 echo esc_html( $method );
3321 ?>
3322 </td>
3323 <td>
3324 <?php
3325 $reliability = (int) $detection['reliability'];
3326
3327 if ( $reliability > 0 ) {
3328 $reliability_class = 'low';
3329 if ( $reliability >= 80 ) {
3330 $reliability_class = '';
3331 } elseif ( $reliability >= 50 ) {
3332 $reliability_class = 'medium';
3333 }
3334 ?>
3335 <span class="wpvulnerability-reliability-bar">
3336 <span class="wpvulnerability-reliability-fill <?php echo esc_attr( $reliability_class ); ?>" style="width: <?php echo esc_attr( (string) $reliability ); ?>%;"></span>
3337 </span>
3338 <span><?php echo esc_html( (string) $reliability ); ?>%</span>
3339 <?php
3340 } else {
3341 ?>
3342 <span style="color: #646970;"></span>
3343 <?php
3344 }
3345 ?>
3346 </td>
3347 </tr>
3348 <?php endforeach; ?>
3349 </tbody>
3350 </table>
3351 </div>
3352 <?php
3353 }
3354
3355 /**
3356 * Displays the shell execution audit logs section.
3357 *
3358 * Shows recent shell_exec calls with full details for security auditing.
3359 *
3360 * @since 4.3.0
3361 *
3362 * @return void
3363 */
3364 function wpvulnerability_display_security_logs() {
3365 $per_page = 20;
3366 $logs = wpvulnerability_get_shell_exec_logs( $per_page, 1 );
3367 ?>
3368 <div class="wpvulnerability-security-section">
3369 <h3><?php esc_html_e( 'Shell Execution Audit Logs', 'wpvulnerability' ); ?></h3>
3370 <p><?php esc_html_e( 'Complete audit trail of shell command executions for security monitoring.', 'wpvulnerability' ); ?></p>
3371
3372 <?php if ( empty( $logs ) ) : ?>
3373 <div class="wpvulnerability-empty-state">
3374 <div class="wpvulnerability-empty-state-icon">🔒</div>
3375 <p><strong><?php esc_html_e( 'No shell execution logs found.', 'wpvulnerability' ); ?></strong></p>
3376 <p><?php esc_html_e( 'Logs will appear here when shell commands are executed for software detection.', 'wpvulnerability' ); ?></p>
3377 </div>
3378 <?php else : ?>
3379 <table class="wpvulnerability-logs-table">
3380 <thead>
3381 <tr>
3382 <th><?php esc_html_e( 'Time', 'wpvulnerability' ); ?></th>
3383 <th><?php esc_html_e( 'Component', 'wpvulnerability' ); ?></th>
3384 <th><?php esc_html_e( 'Command', 'wpvulnerability' ); ?></th>
3385 <th><?php esc_html_e( 'Status', 'wpvulnerability' ); ?></th>
3386 <th><?php esc_html_e( 'User', 'wpvulnerability' ); ?></th>
3387 <th><?php esc_html_e( 'Output', 'wpvulnerability' ); ?></th>
3388 </tr>
3389 </thead>
3390 <tbody>
3391 <?php foreach ( $logs as $log ) : ?>
3392 <?php
3393 $log_data = json_decode( $log->post_content, true );
3394 if ( ! is_array( $log_data ) ) {
3395 $log_data = array();
3396 }
3397 if ( empty( $log_data ) ) {
3398 continue;
3399 }
3400 $log_component = isset( $log_data['component'] ) && is_string( $log_data['component'] ) ? $log_data['component'] : '';
3401 $log_command = isset( $log_data['command'] ) && is_string( $log_data['command'] ) ? $log_data['command'] : '';
3402 $log_reason = isset( $log_data['reason'] ) && is_string( $log_data['reason'] ) ? $log_data['reason'] : '';
3403 $log_user = isset( $log_data['user'] ) && is_string( $log_data['user'] ) ? $log_data['user'] : '';
3404 $log_output = isset( $log_data['output'] ) && is_string( $log_data['output'] ) ? $log_data['output'] : '';
3405 $log_success = ! empty( $log_data['success'] );
3406 ?>
3407 <tr>
3408 <td>
3409 <?php
3410 echo esc_html(
3411 sprintf(
3412 /* translators: %s: time ago */
3413 __( '%s ago', 'wpvulnerability' ),
3414 human_time_diff( absint( strtotime( $log->post_date ) ), time() )
3415 )
3416 );
3417 ?>
3418 </td>
3419 <td><code><?php echo esc_html( $log_component ); ?></code></td>
3420 <td><code><?php echo esc_html( $log_command ); ?></code></td>
3421 <td>
3422 <?php if ( $log_success ) : ?>
3423 <span class="wpvulnerability-status-badge success"><?php echo esc_html( ucfirst( $log_reason ) ); ?></span>
3424 <?php else : ?>
3425 <span class="wpvulnerability-status-badge warning"><?php echo esc_html( ucfirst( $log_reason ) ); ?></span>
3426 <?php endif; ?>
3427 </td>
3428 <td><?php echo esc_html( $log_user ); ?></td>
3429 <td>
3430 <?php if ( '' !== $log_output ) : ?>
3431 <div class="wpvulnerability-log-output" title="<?php echo esc_attr( $log_output ); ?>">
3432 <?php echo esc_html( substr( $log_output, 0, 50 ) ); ?>
3433 <?php if ( strlen( $log_output ) > 50 ) : ?>
3434 <span>...</span>
3435 <?php endif; ?>
3436 </div>
3437 <?php else : ?>
3438 <span style="color: #646970;"></span>
3439 <?php endif; ?>
3440 </td>
3441 </tr>
3442 <?php endforeach; ?>
3443 </tbody>
3444 </table>
3445
3446 <p style="margin-top: 15px; color: #646970; font-size: 13px;">
3447 <?php
3448 /* translators: %d: number of logs shown */
3449 echo esc_html( sprintf( __( 'Showing the %d most recent log entries.', 'wpvulnerability' ), $per_page ) );
3450 ?>
3451 </p>
3452 <?php endif; ?>
3453 </div>
3454 <?php
3455 }
3456
3457 /**
3458 * AJAX handler for testing API connectivity.
3459 *
3460 * @since 4.3.0
3461 *
3462 * @return void
3463 */
3464
3465 /**
3466 * Outputs the Debug tab contents for the multisite network admin.
3467 *
3468 * Reuses the single-site debug display functions since the functionality
3469 * is identical for both single-site and multisite contexts.
3470 *
3471 * @since 4.3.0
3472 *
3473 * @return void
3474 */
3475 function wpvulnerability_render_network_admin_tab_debug() {
3476 // Security check: only show in debug mode.
3477 if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
3478 return;
3479 }
3480
3481 // Check permissions for multisite.
3482 if ( ! current_user_can( 'manage_network_options' ) ) {
3483 echo '<div class="notice notice-error"><p>';
3484 esc_html_e( 'You do not have permission to access this page.', 'wpvulnerability' );
3485 echo '</p></div>';
3486 return;
3487 }
3488
3489 // Load debug functions if not already loaded.
3490 if ( ! function_exists( 'wpvulnerability_debug_get_system_info' ) ) {
3491 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-debug.php';
3492 }
3493
3494 ?>
3495 <div class="wpvulnerability-section">
3496 <h2><?php esc_html_e( 'Debug Information', 'wpvulnerability' ); ?></h2>
3497 <p style="color: #d63638; font-weight: 500;">
3498 <?php esc_html_e( 'This tab is only visible when WP_DEBUG is enabled. Use with caution.', 'wpvulnerability' ); ?>
3499 </p>
3500
3501 <?php
3502 // Section 1: System Information.
3503 wpvulnerability_render_debug_section_system_info();
3504
3505 // Section 2: Component Detection.
3506 wpvulnerability_render_debug_section_components();
3507
3508 // Section 4: Configuration Summary.
3509 wpvulnerability_render_debug_section_config();
3510
3511 // Section 5: Cron Status.
3512 wpvulnerability_render_debug_section_cron();
3513
3514 // Section 3: API Testing.
3515 wpvulnerability_render_debug_section_api_testing();
3516
3517 // Section 6: Database Options Viewer.
3518 wpvulnerability_render_debug_section_database_options();
3519
3520 // Section 7: Quick Actions.
3521 wpvulnerability_render_debug_section_quick_actions();
3522 ?>
3523 </div>
3524 <?php
3525 }
3526
3527 /**
3528 * AJAX handler for testing API connectivity in multisite.
3529 *
3530 * @since 4.3.0
3531 *
3532 * @return void
3533 */
3534 function wpvulnerability_ajax_network_test_api() {
3535 // Verify nonce.
3536 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( is_string( $_POST['nonce'] ) ? $_POST['nonce'] : '' ), 'wpvulnerability_test_api' ) ) {
3537 wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wpvulnerability' ) ) );
3538 }
3539
3540 // Check permissions.
3541 if ( ! current_user_can( 'manage_network_options' ) ) {
3542 wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'wpvulnerability' ) ) );
3543 }
3544
3545 // Get component.
3546 $component = isset( $_POST['component'] ) ? sanitize_key( is_string( $_POST['component'] ) ? $_POST['component'] : '' ) : '';
3547 if ( empty( $component ) ) {
3548 wp_send_json_error( array( 'message' => __( 'No component specified.', 'wpvulnerability' ) ) );
3549 }
3550
3551 // Load debug functions.
3552 if ( ! function_exists( 'wpvulnerability_debug_test_api_component' ) ) {
3553 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-debug.php';
3554 }
3555
3556 // Test API.
3557 $wpvulnerability_result = wpvulnerability_debug_test_api_component( $component );
3558
3559 wp_send_json_success( $wpvulnerability_result );
3560 }
3561 add_action( 'wp_ajax_wpvulnerability_test_api', 'wpvulnerability_ajax_network_test_api' );
3562
3563 /**
3564 * Renders Section 1: System Information.
3565 *
3566 * @since 4.3.0
3567 *
3568 * @return void
3569 */
3570 function wpvulnerability_render_debug_section_system_info() {
3571 $system_info = wpvulnerability_debug_get_system_info();
3572
3573 // Extract typed sub-arrays from the mixed return value.
3574 $si_wordpress = isset( $system_info['wordpress'] ) && is_array( $system_info['wordpress'] ) ? $system_info['wordpress'] : array();
3575 $si_php = isset( $system_info['php'] ) && is_array( $system_info['php'] ) ? $system_info['php'] : array();
3576 $si_database = isset( $system_info['database'] ) && is_array( $system_info['database'] ) ? $system_info['database'] : array();
3577 $si_webserver = isset( $system_info['webserver'] ) && is_array( $system_info['webserver'] ) ? $system_info['webserver'] : array();
3578 $si_debug = isset( $system_info['debug'] ) && is_array( $system_info['debug'] ) ? $system_info['debug'] : array();
3579
3580 $si_wp_version = isset( $si_wordpress['version'] ) && is_scalar( $si_wordpress['version'] ) ? (string) $si_wordpress['version'] : '';
3581 $si_multisite = ! empty( $si_wordpress['multisite'] );
3582 $si_language = isset( $si_wordpress['language'] ) && is_scalar( $si_wordpress['language'] ) ? (string) $si_wordpress['language'] : '';
3583 $si_php_version = isset( $si_php['version'] ) && is_scalar( $si_php['version'] ) ? (string) $si_php['version'] : '';
3584 $si_extensions = isset( $si_php['extensions'] ) && is_array( $si_php['extensions'] ) ? $si_php['extensions'] : array();
3585 $si_memory = isset( $si_php['memory'] ) && is_array( $si_php['memory'] ) ? $si_php['memory'] : array();
3586 $si_mem_limit = isset( $si_memory['limit'] ) && is_scalar( $si_memory['limit'] ) ? (string) $si_memory['limit'] : '';
3587 $si_db_type = isset( $si_database['type'] ) && is_scalar( $si_database['type'] ) ? (string) $si_database['type'] : '';
3588 $si_db_version = isset( $si_database['version'] ) && is_scalar( $si_database['version'] ) ? (string) $si_database['version'] : '';
3589 $si_webserver_s = isset( $si_webserver['software'] ) && is_scalar( $si_webserver['software'] ) ? (string) $si_webserver['software'] : '';
3590 $si_wp_debug = ! empty( $si_debug['wp_debug'] );
3591 $si_debug_log = ! empty( $si_debug['wp_debug_log'] );
3592 $si_log_file = isset( $si_debug['log_file'] ) && is_array( $si_debug['log_file'] ) ? $si_debug['log_file'] : array();
3593 ?>
3594 <div class="wpvulnerability-subsection" style="margin-top: 20px;">
3595 <h3><?php esc_html_e( 'System Information', 'wpvulnerability' ); ?></h3>
3596 <table class="widefat" style="margin-top: 10px;">
3597 <tbody>
3598 <tr>
3599 <td style="width: 200px; font-weight: 600;"><?php esc_html_e( 'WordPress Version', 'wpvulnerability' ); ?></td>
3600 <td><?php echo esc_html( $si_wp_version ); ?></td>
3601 </tr>
3602 <tr>
3603 <td style="font-weight: 600;"><?php esc_html_e( 'Multisite', 'wpvulnerability' ); ?></td>
3604 <td><?php echo $si_multisite ? esc_html__( 'Yes', 'wpvulnerability' ) : esc_html__( 'No', 'wpvulnerability' ); ?></td>
3605 </tr>
3606 <tr>
3607 <td style="font-weight: 600;"><?php esc_html_e( 'Language', 'wpvulnerability' ); ?></td>
3608 <td><?php echo esc_html( $si_language ); ?></td>
3609 </tr>
3610 <tr>
3611 <td style="font-weight: 600;"><?php esc_html_e( 'PHP Version', 'wpvulnerability' ); ?></td>
3612 <td><?php echo esc_html( $si_php_version ); ?></td>
3613 </tr>
3614 <tr>
3615 <td style="font-weight: 600;"><?php esc_html_e( 'PHP Extensions', 'wpvulnerability' ); ?></td>
3616 <td>
3617 <?php foreach ( $si_extensions as $ext => $loaded ) : ?>
3618 <span style="margin-right: 10px;">
3619 <span style="color: <?php echo $loaded ? '#00a32a' : '#d63638'; ?>;"></span>
3620 <?php echo esc_html( (string) $ext ); ?>
3621 </span>
3622 <?php endforeach; ?>
3623 </td>
3624 </tr>
3625 <tr>
3626 <td style="font-weight: 600;"><?php esc_html_e( 'PHP Memory Limit', 'wpvulnerability' ); ?></td>
3627 <td><?php echo esc_html( $si_mem_limit ); ?></td>
3628 </tr>
3629 <tr>
3630 <td style="font-weight: 600;"><?php esc_html_e( 'Database', 'wpvulnerability' ); ?></td>
3631 <td><?php echo esc_html( $si_db_type . ' ' . $si_db_version ); ?></td>
3632 </tr>
3633 <tr>
3634 <td style="font-weight: 600;"><?php esc_html_e( 'Web Server', 'wpvulnerability' ); ?></td>
3635 <td><?php echo esc_html( $si_webserver_s ); ?></td>
3636 </tr>
3637 <tr>
3638 <td style="font-weight: 600;"><?php esc_html_e( 'WP_DEBUG', 'wpvulnerability' ); ?></td>
3639 <td>
3640 <span style="color: <?php echo $si_wp_debug ? '#00a32a' : '#d63638'; ?>; font-weight: 600;">
3641 <?php echo $si_wp_debug ? esc_html__( 'Enabled', 'wpvulnerability' ) : esc_html__( 'Disabled', 'wpvulnerability' ); ?>
3642 </span>
3643 </td>
3644 </tr>
3645 <tr>
3646 <td style="font-weight: 600;"><?php esc_html_e( 'WP_DEBUG_LOG', 'wpvulnerability' ); ?></td>
3647 <td>
3648 <span style="color: <?php echo $si_debug_log ? '#00a32a' : '#d63638'; ?>; font-weight: 600;">
3649 <?php echo $si_debug_log ? esc_html__( 'Enabled', 'wpvulnerability' ) : esc_html__( 'Disabled', 'wpvulnerability' ); ?>
3650 </span>
3651 <?php if ( $si_debug_log && ! empty( $si_log_file ) ) : ?>
3652 <?php $log_file = $si_log_file; ?>
3653 <?php if ( ! empty( $log_file['path'] ) ) : ?>
3654 <br>
3655 <span style="color: #646970; font-size: 12px;">
3656 <?php esc_html_e( 'Path:', 'wpvulnerability' ); ?>
3657 <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;"><?php echo esc_html( is_scalar( $log_file['path'] ) ? (string) $log_file['path'] : '' ); ?></code>
3658 </span>
3659 <?php
3660 $lf_exists = ! empty( $log_file['exists'] );
3661 $lf_size = isset( $log_file['size'] ) && is_scalar( $log_file['size'] ) ? (int) $log_file['size'] : 0;
3662 $lf_accessible = ! empty( $log_file['accessible'] );
3663 $lf_url = isset( $log_file['url'] ) && is_scalar( $log_file['url'] ) ? (string) $log_file['url'] : '';
3664 ?>
3665 <?php if ( $lf_exists ) : ?>
3666 <br>
3667 <span style="color: #646970; font-size: 12px;">
3668 <?php
3669 /* translators: %s: file size */
3670 echo esc_html( sprintf( __( 'Size: %s', 'wpvulnerability' ), size_format( $lf_size ) ) );
3671 ?>
3672 </span>
3673 <?php if ( $lf_accessible && '' !== $lf_url ) : ?>
3674 <br>
3675 <a href="<?php echo esc_url( $lf_url ); ?>" target="_blank" rel="noopener noreferrer" class="button button-small" style="margin-top: 5px;">
3676 <?php esc_html_e( 'View Log File', 'wpvulnerability' ); ?>
3677 <span class="dashicons dashicons-external" style="font-size: 13px; margin-top: 3px;"></span>
3678 </a>
3679 <?php elseif ( $lf_exists ) : ?>
3680 <br>
3681 <span style="color: #d63638; font-size: 12px;">
3682 <?php esc_html_e( 'Log file is not web-accessible (outside wp-content)', 'wpvulnerability' ); ?>
3683 </span>
3684 <?php endif; ?>
3685 <?php else : ?>
3686 <br>
3687 <span style="color: #646970; font-size: 12px;">
3688 <?php esc_html_e( 'Log file does not exist yet', 'wpvulnerability' ); ?>
3689 </span>
3690 <?php endif; ?>
3691 <?php endif; ?>
3692 <?php endif; ?>
3693 </td>
3694 </tr>
3695 <tr>
3696 <td style="font-weight: 600;"><?php esc_html_e( 'Plugin Version', 'wpvulnerability' ); ?></td>
3697 <?php
3698 $si_plugin = isset( $system_info['plugin'] ) && is_array( $system_info['plugin'] ) ? $system_info['plugin'] : array();
3699 $si_plug_ver = isset( $si_plugin['version'] ) && is_scalar( $si_plugin['version'] ) ? (string) $si_plugin['version'] : '';
3700 ?>
3701 <td><?php echo esc_html( $si_plug_ver ); ?></td>
3702 </tr>
3703 </tbody>
3704 </table>
3705 </div>
3706 <?php
3707 }
3708
3709 /**
3710 * Renders Section 2: Component Detection.
3711 *
3712 * @since 4.3.0
3713 *
3714 * @return void
3715 */
3716 function wpvulnerability_render_debug_section_components() {
3717 $components = wpvulnerability_debug_get_component_status();
3718 ?>
3719 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
3720 <h3><?php esc_html_e( 'Component Detection', 'wpvulnerability' ); ?></h3>
3721 <table class="widefat striped" style="margin-top: 10px;">
3722 <thead>
3723 <tr>
3724 <th><?php esc_html_e( 'Component', 'wpvulnerability' ); ?></th>
3725 <th><?php esc_html_e( 'Status', 'wpvulnerability' ); ?></th>
3726 <th><?php esc_html_e( 'Version Detected', 'wpvulnerability' ); ?></th>
3727 <th><?php esc_html_e( 'Analyzed', 'wpvulnerability' ); ?></th>
3728 <th><?php esc_html_e( 'Cache Status', 'wpvulnerability' ); ?></th>
3729 </tr>
3730 </thead>
3731 <tbody>
3732 <?php foreach ( $components as $comp ) : ?>
3733 <?php
3734 $comp_name = isset( $comp['component'] ) && is_scalar( $comp['component'] ) ? (string) $comp['component'] : '';
3735 $comp_detected = ! empty( $comp['detected'] );
3736 $comp_version = isset( $comp['version'] ) && is_scalar( $comp['version'] ) ? (string) $comp['version'] : '';
3737 $comp_analyzed = ! empty( $comp['analyzed'] );
3738 $comp_cache_status = isset( $comp['cache_status'] ) && is_scalar( $comp['cache_status'] ) ? (string) $comp['cache_status'] : '';
3739 ?>
3740 <tr>
3741 <td style="font-weight: 600;"><?php echo esc_html( ucfirst( $comp_name ) ); ?></td>
3742 <td>
3743 <span style="color: <?php echo $comp_detected ? '#00a32a' : '#d63638'; ?>; font-size: 16px;">
3744 <?php echo $comp_detected ? '' : ''; ?>
3745 </span>
3746 </td>
3747 <td><?php echo esc_html( $comp_version ); ?></td>
3748 <td>
3749 <span style="color: <?php echo $comp_analyzed ? '#00a32a' : '#d63638'; ?>;">
3750 <?php echo $comp_analyzed ? esc_html__( 'Yes', 'wpvulnerability' ) : esc_html__( 'No', 'wpvulnerability' ); ?>
3751 </span>
3752 </td>
3753 <td>
3754 <?php
3755 $badge_color = '#646970';
3756 if ( strpos( $comp_cache_status, 'Fresh' ) !== false ) {
3757 $badge_color = '#00a32a';
3758 } elseif ( strpos( $comp_cache_status, 'Expired' ) !== false ) {
3759 $badge_color = '#d63638';
3760 }
3761 ?>
3762 <span style="background: <?php echo esc_attr( $badge_color ); ?>; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px; font-weight: 600;">
3763 <?php echo esc_html( $comp_cache_status ); ?>
3764 </span>
3765 </td>
3766 </tr>
3767 <?php endforeach; ?>
3768 </tbody>
3769 </table>
3770 </div>
3771 <?php
3772 }
3773
3774 /**
3775 * Renders Section 4: Configuration Summary.
3776 *
3777 * @since 4.3.0
3778 *
3779 * @return void
3780 */
3781 function wpvulnerability_render_debug_section_config() {
3782 $config_raw = is_multisite()
3783 ? get_site_option( 'wpvulnerability-config', array() )
3784 : get_option( 'wpvulnerability-config', array() );
3785 $config = is_array( $config_raw ) ? $config_raw : array();
3786 ?>
3787 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
3788 <h3><?php esc_html_e( 'Configuration Summary', 'wpvulnerability' ); ?></h3>
3789 <table class="widefat" style="margin-top: 10px;">
3790 <tbody>
3791 <tr>
3792 <td style="width: 250px; font-weight: 600;"><?php esc_html_e( 'Cache Duration', 'wpvulnerability' ); ?></td>
3793 <td>
3794 <?php
3795 $cache_hours = isset( $config['cache'] ) && is_scalar( $config['cache'] ) ? absint( (int) $config['cache'] ) : 12;
3796 /* translators: %d: number of hours */
3797 echo esc_html( sprintf( _n( '%d hour', '%d hours', $cache_hours, 'wpvulnerability' ), $cache_hours ) );
3798 ?>
3799 </td>
3800 </tr>
3801 <tr>
3802 <td style="font-weight: 600;"><?php esc_html_e( 'Log Retention', 'wpvulnerability' ); ?></td>
3803 <td>
3804 <?php
3805 $retention_days = isset( $config['log_retention_days'] ) && is_scalar( $config['log_retention_days'] ) ? absint( (int) $config['log_retention_days'] ) : 14;
3806 /* translators: %d: Number of days. */
3807 echo esc_html( sprintf( _n( '%d day', '%d days', $retention_days, 'wpvulnerability' ), $retention_days ) );
3808 ?>
3809 </td>
3810 </tr>
3811 <tr>
3812 <td style="font-weight: 600;"><?php esc_html_e( 'Components Analyzed', 'wpvulnerability' ); ?></td>
3813 <td>
3814 <?php
3815 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
3816 $analyzed_count = 0;
3817 foreach ( $components as $component ) {
3818 if ( wpvulnerability_analyze_filter( $component ) ) {
3819 ++$analyzed_count;
3820 }
3821 }
3822 echo esc_html( sprintf( '%d / %d', $analyzed_count, count( $components ) ) );
3823 ?>
3824 </td>
3825 </tr>
3826 <tr>
3827 <td style="font-weight: 600;"><?php esc_html_e( 'Notification Email', 'wpvulnerability' ); ?></td>
3828 <td><?php echo isset( $config['emails'] ) && is_scalar( $config['emails'] ) ? esc_html( (string) $config['emails'] ) : ''; ?></td>
3829 </tr>
3830 <tr>
3831 <td style="font-weight: 600;"><?php esc_html_e( 'Notification Frequency', 'wpvulnerability' ); ?></td>
3832 <td>
3833 <?php
3834 $period = isset( $config['period'] ) && is_scalar( $config['period'] ) ? (string) $config['period'] : 'weekly';
3835 echo esc_html( ucfirst( $period ) );
3836 if ( 'weekly' === $period && isset( $config['day'] ) ) {
3837 $config_day_raw = $config['day'];
3838 $config_day = is_scalar( $config_day_raw ) ? (string) $config_day_raw : 'monday';
3839 echo ' (' . esc_html( ucfirst( $config_day ) ) . ')';
3840 }
3841 if ( isset( $config['hour'] ) && isset( $config['minute'] ) ) {
3842 echo ' ' . esc_html( sprintf( '%02d:%02d', absint( is_scalar( $config['hour'] ) ? (int) $config['hour'] : 0 ), absint( is_scalar( $config['minute'] ) ? (int) $config['minute'] : 0 ) ) );
3843 }
3844 ?>
3845 </td>
3846 </tr>
3847 <tr>
3848 <td style="font-weight: 600;"><?php esc_html_e( 'Slack Webhook', 'wpvulnerability' ); ?></td>
3849 <td>
3850 <?php
3851 $config_notify = isset( $config['notify'] ) && is_array( $config['notify'] ) ? $config['notify'] : array();
3852 $slack_configured = ! empty( $config['slack_webhook'] );
3853 $slack_enabled = isset( $config_notify['slack'] ) && 'y' === $config_notify['slack'];
3854 ?>
3855 <span style="color: <?php echo ( $slack_configured && $slack_enabled ) ? '#00a32a' : '#646970'; ?>;">
3856 <?php
3857 if ( $slack_configured && $slack_enabled ) {
3858 esc_html_e( 'Configured & Enabled', 'wpvulnerability' );
3859 } elseif ( $slack_configured ) {
3860 esc_html_e( 'Configured (Disabled)', 'wpvulnerability' );
3861 } else {
3862 esc_html_e( 'Not configured', 'wpvulnerability' );
3863 }
3864 ?>
3865 </span>
3866 </td>
3867 </tr>
3868 <tr>
3869 <td style="font-weight: 600;"><?php esc_html_e( 'Teams Webhook', 'wpvulnerability' ); ?></td>
3870 <td>
3871 <?php
3872 $teams_configured = ! empty( $config['teams_webhook'] );
3873 $teams_enabled = isset( $config_notify['teams'] ) && 'y' === $config_notify['teams'];
3874 ?>
3875 <span style="color: <?php echo ( $teams_configured && $teams_enabled ) ? '#00a32a' : '#646970'; ?>;">
3876 <?php
3877 if ( $teams_configured && $teams_enabled ) {
3878 esc_html_e( 'Configured & Enabled', 'wpvulnerability' );
3879 } elseif ( $teams_configured ) {
3880 esc_html_e( 'Configured (Disabled)', 'wpvulnerability' );
3881 } else {
3882 esc_html_e( 'Not configured', 'wpvulnerability' );
3883 }
3884 ?>
3885 </span>
3886 </td>
3887 </tr>
3888 <tr>
3889 <td style="font-weight: 600;"><?php esc_html_e( 'Discord Webhook', 'wpvulnerability' ); ?></td>
3890 <td>
3891 <?php
3892 $discord_configured = ! empty( $config['discord_webhook'] );
3893 $discord_enabled = isset( $config_notify['discord'] ) && 'y' === $config_notify['discord'];
3894 ?>
3895 <span style="color: <?php echo ( $discord_configured && $discord_enabled ) ? '#00a32a' : '#646970'; ?>;">
3896 <?php
3897 if ( $discord_configured && $discord_enabled ) {
3898 esc_html_e( 'Configured & Enabled', 'wpvulnerability' );
3899 } elseif ( $discord_configured ) {
3900 esc_html_e( 'Configured (Disabled)', 'wpvulnerability' );
3901 } else {
3902 esc_html_e( 'Not configured', 'wpvulnerability' );
3903 }
3904 ?>
3905 </span>
3906 </td>
3907 </tr>
3908 <tr>
3909 <td style="font-weight: 600;"><?php esc_html_e( 'Telegram Bot', 'wpvulnerability' ); ?></td>
3910 <td>
3911 <?php
3912 $telegram_configured = ! empty( $config['telegram_bot_token'] ) && ! empty( $config['telegram_chat_id'] );
3913 $telegram_enabled = isset( $config_notify['telegram'] ) && 'y' === $config_notify['telegram'];
3914 ?>
3915 <span style="color: <?php echo ( $telegram_configured && $telegram_enabled ) ? '#00a32a' : '#646970'; ?>;">
3916 <?php
3917 if ( $telegram_configured && $telegram_enabled ) {
3918 esc_html_e( 'Configured & Enabled', 'wpvulnerability' );
3919 } elseif ( $telegram_configured ) {
3920 esc_html_e( 'Configured (Disabled)', 'wpvulnerability' );
3921 } else {
3922 esc_html_e( 'Not configured', 'wpvulnerability' );
3923 }
3924 ?>
3925 </span>
3926 </td>
3927 </tr>
3928 </tbody>
3929 </table>
3930 </div>
3931 <?php
3932 }
3933
3934 /**
3935 * Renders Section 5: Cron Status.
3936 *
3937 * @since 4.3.0
3938 *
3939 * @return void
3940 */
3941 function wpvulnerability_render_debug_section_cron() {
3942 $cron_status = wpvulnerability_debug_get_cron_status();
3943 $can_manage = current_user_can( 'manage_network_options' );
3944
3945 // Extract typed values from mixed cron status arrays.
3946 $cs_update = isset( $cron_status['update_database'] ) ? (array) $cron_status['update_database'] : array();
3947 $cs_notify = isset( $cron_status['send_notification'] ) ? (array) $cron_status['send_notification'] : array();
3948 $upd_scheduled = ! empty( $cs_update['scheduled'] );
3949 $upd_next_run = isset( $cs_update['next_run'] ) && is_numeric( $cs_update['next_run'] ) ? (int) $cs_update['next_run'] : null;
3950 $notif_scheduled = ! empty( $cs_notify['scheduled'] );
3951 $notif_next_run = isset( $cs_notify['next_run'] ) && is_numeric( $cs_notify['next_run'] ) ? (int) $cs_notify['next_run'] : null;
3952 ?>
3953 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
3954 <h3><?php esc_html_e( 'Cron Status', 'wpvulnerability' ); ?></h3>
3955 <table class="widefat" style="margin-top: 10px;">
3956 <tbody>
3957 <tr>
3958 <td style="width: 250px; font-weight: 600;"><?php esc_html_e( 'Update Database', 'wpvulnerability' ); ?></td>
3959 <td>
3960 <?php if ( $upd_scheduled && null !== $upd_next_run ) : ?>
3961 <?php
3962 /* translators: %s: date and time */
3963 echo esc_html( sprintf( __( 'Next run: %s', 'wpvulnerability' ), wp_date( 'Y-m-d H:i:s', $upd_next_run ) ) );
3964 ?>
3965 <?php else : ?>
3966 <span style="color: #d63638;"><?php esc_html_e( 'Not scheduled', 'wpvulnerability' ); ?></span>
3967 <?php endif; ?>
3968 </td>
3969 </tr>
3970 <tr>
3971 <td style="font-weight: 600;"><?php esc_html_e( 'Send Notification', 'wpvulnerability' ); ?></td>
3972 <td>
3973 <?php if ( $notif_scheduled && null !== $notif_next_run ) : ?>
3974 <?php
3975 /* translators: %s: date and time */
3976 echo esc_html( sprintf( __( 'Next run: %s', 'wpvulnerability' ), wp_date( 'Y-m-d H:i:s', $notif_next_run ) ) );
3977 ?>
3978 <?php else : ?>
3979 <span style="color: #646970;"><?php esc_html_e( 'Not scheduled', 'wpvulnerability' ); ?></span>
3980 <?php endif; ?>
3981 </td>
3982 </tr>
3983 </tbody>
3984 </table>
3985
3986 <?php if ( $can_manage ) : ?>
3987 <div style="margin-top: 15px;">
3988 <form method="post" style="display: inline-block; margin-right: 10px;">
3989 <?php wp_nonce_field( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ); ?>
3990 <input type="hidden" name="wpvulnerability_run_update" value="1">
3991 <button type="submit" class="button button-secondary">
3992 <?php esc_html_e( 'Run Update Now', 'wpvulnerability' ); ?>
3993 </button>
3994 </form>
3995
3996 <form method="post" style="display: inline-block;">
3997 <?php wp_nonce_field( 'wpvulnerability_cron_action', 'wpvulnerability_cron_nonce' ); ?>
3998 <input type="hidden" name="wpvulnerability_run_notification" value="1">
3999 <button type="submit" class="button button-secondary">
4000 <?php esc_html_e( 'Run Notification Now', 'wpvulnerability' ); ?>
4001 </button>
4002 </form>
4003 </div>
4004 <?php else : ?>
4005 <p style="color: #d63638; margin-top: 10px;">
4006 <?php esc_html_e( 'You do not have permission to run these actions.', 'wpvulnerability' ); ?>
4007 </p>
4008 <?php endif; ?>
4009 </div>
4010 <?php
4011 }
4012
4013 /**
4014 * Renders Section 3: API Testing.
4015 *
4016 * @since 4.3.0
4017 *
4018 * @return void
4019 */
4020 function wpvulnerability_render_debug_section_api_testing() {
4021 $can_manage = current_user_can( 'manage_network_options' );
4022 $components = array( 'core', 'plugins', 'themes', 'php', 'apache', 'nginx', 'mysql', 'mariadb', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' );
4023 ?>
4024 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
4025 <h3><?php esc_html_e( 'API Testing', 'wpvulnerability' ); ?></h3>
4026
4027 <?php if ( $can_manage ) : ?>
4028 <p><?php esc_html_e( 'Test API connectivity for each component. Click a button to send a test request.', 'wpvulnerability' ); ?></p>
4029
4030 <div id="wpvulnerability-api-test-buttons" style="margin-top: 15px;">
4031 <?php foreach ( $components as $component ) : ?>
4032 <button type="button" class="button button-secondary wpvulnerability-test-api-btn" data-component="<?php echo esc_attr( $component ); ?>" style="margin: 5px;">
4033 <?php
4034 /* translators: %s: component name */
4035 echo esc_html( sprintf( __( 'Test %s', 'wpvulnerability' ), ucfirst( $component ) ) );
4036 ?>
4037 </button>
4038 <?php endforeach; ?>
4039 </div>
4040
4041 <div id="wpvulnerability-api-test-results" style="margin-top: 20px;"></div>
4042
4043 <script type="text/javascript">
4044 jQuery(document).ready(function($) {
4045 $('.wpvulnerability-test-api-btn').on('click', function() {
4046 var $btn = $(this);
4047 var component = $btn.data('component');
4048 var $results = $('#wpvulnerability-api-test-results');
4049
4050 $btn.prop('disabled', true).text('<?php echo esc_js( __( 'Testing...', 'wpvulnerability' ) ); ?>');
4051
4052 $.ajax({
4053 url: typeof ajaxurl !== 'undefined' ? ajaxurl : wpvulnerabilityAjax.ajaxurl,
4054 type: 'POST',
4055 data: {
4056 action: 'wpvulnerability_test_api',
4057 component: component,
4058 nonce: '<?php echo esc_js( wp_create_nonce( 'wpvulnerability_test_api' ) ); ?>'
4059 },
4060 success: function(response) {
4061 if (response.success) {
4062 var result = response.data;
4063 var statusColor = result.success ? '#00a32a' : '#d63638';
4064 var resultHtml = '<div style="border: 1px solid ' + statusColor + '; padding: 15px; margin-top: 10px; border-radius: 4px;">';
4065 resultHtml += '<h4 style="margin-top: 0; color: ' + statusColor + ';">' + component.toUpperCase() + ' - ' + result.message + '</h4>';
4066 resultHtml += '<p><strong><?php echo esc_js( __( 'HTTP Code:', 'wpvulnerability' ) ); ?></strong> ' + result.http_code + '</p>';
4067 resultHtml += '<p><strong><?php echo esc_js( __( 'Response Time:', 'wpvulnerability' ) ); ?></strong> ' + result.response_time + ' ms</p>';
4068 if (result.data_preview) {
4069 resultHtml += '<details style="margin-top: 10px;"><summary style="cursor: pointer; font-weight: 600;"><?php echo esc_js( __( 'Response Preview', 'wpvulnerability' ) ); ?></summary>';
4070 resultHtml += '<pre style="background: #f0f0f0; padding: 10px; overflow-x: auto; margin-top: 10px;">' + result.data_preview + '</pre>';
4071 resultHtml += '</details>';
4072 }
4073 resultHtml += '</div>';
4074 $results.prepend(resultHtml);
4075 }
4076 $btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1));
4077 },
4078 error: function() {
4079 $results.prepend('<div style="border: 1px solid #d63638; padding: 15px; margin-top: 10px; border-radius: 4px; color: #d63638;"><strong><?php echo esc_js( __( 'Error:', 'wpvulnerability' ) ); ?></strong> <?php echo esc_js( __( 'AJAX request failed.', 'wpvulnerability' ) ); ?></div>');
4080 $btn.prop('disabled', false).text('<?php echo esc_js( __( 'Test', 'wpvulnerability' ) ); ?> ' + component.charAt(0).toUpperCase() + component.slice(1));
4081 }
4082 });
4083 });
4084 });
4085 </script>
4086 <?php else : ?>
4087 <p style="color: #d63638;">
4088 <?php esc_html_e( 'You do not have permission to test API connectivity.', 'wpvulnerability' ); ?>
4089 </p>
4090 <?php endif; ?>
4091 </div>
4092 <?php
4093 }
4094
4095 /**
4096 * Renders Section 6: Database Options Viewer.
4097 *
4098 * @since 4.3.0
4099 *
4100 * @return void
4101 */
4102 function wpvulnerability_render_debug_section_database_options() {
4103 $option_names = wpvulnerability_debug_get_option_names();
4104 $selected_option = filter_input( INPUT_GET, 'debug_option', FILTER_SANITIZE_SPECIAL_CHARS );
4105 $selected_option = $selected_option ? sanitize_key( $selected_option ) : '';
4106 $page_value = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS );
4107 ?>
4108 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
4109 <h3><?php esc_html_e( 'Database Options Viewer', 'wpvulnerability' ); ?></h3>
4110 <p><?php esc_html_e( 'View the raw data stored in WordPress options.', 'wpvulnerability' ); ?></p>
4111
4112 <form method="get" style="margin-top: 15px;">
4113 <input type="hidden" name="page" value="<?php echo $page_value ? esc_attr( $page_value ) : ''; ?>">
4114 <input type="hidden" name="tab" value="debug">
4115 <select name="debug_option" style="min-width: 300px;">
4116 <option value=""><?php esc_html_e( 'Select an option...', 'wpvulnerability' ); ?></option>
4117 <?php foreach ( $option_names as $option ) : ?>
4118 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $selected_option, $option ); ?>>
4119 <?php echo esc_html( $option ); ?>
4120 </option>
4121 <?php endforeach; ?>
4122 </select>
4123 <button type="submit" class="button button-secondary"><?php esc_html_e( 'View', 'wpvulnerability' ); ?></button>
4124 </form>
4125
4126 <?php if ( $selected_option && in_array( $selected_option, $option_names, true ) ) : ?>
4127 <?php
4128 $option_value = wpvulnerability_debug_get_option_value( $selected_option );
4129 ?>
4130 <div style="margin-top: 20px;">
4131 <h4><?php echo esc_html( $selected_option ); ?></h4>
4132 <?php if ( null !== $option_value ) : ?>
4133 <pre style="background: #f0f0f0; padding: 15px; overflow-x: auto; border: 1px solid #ddd; border-radius: 4px;"><?php echo esc_html( (string) wp_json_encode( $option_value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); ?></pre>
4134 <?php else : ?>
4135 <p style="color: #646970;"><?php esc_html_e( 'Option not found or empty.', 'wpvulnerability' ); ?></p>
4136 <?php endif; ?>
4137 </div>
4138 <?php endif; ?>
4139 </div>
4140 <?php
4141 }
4142
4143 /**
4144 * Renders Section 7: Quick Actions.
4145 *
4146 * @since 4.3.0
4147 *
4148 * @return void
4149 */
4150 function wpvulnerability_render_debug_section_quick_actions() {
4151 $can_manage = current_user_can( 'manage_network_options' );
4152 ?>
4153 <div class="wpvulnerability-subsection" style="margin-top: 30px;">
4154 <h3><?php esc_html_e( 'Quick Actions', 'wpvulnerability' ); ?></h3>
4155 <p><?php esc_html_e( 'Perform debugging actions. Use with caution.', 'wpvulnerability' ); ?></p>
4156
4157 <?php if ( $can_manage ) : ?>
4158 <div style="margin-top: 15px;">
4159 <form method="post" style="display: inline-block; margin-right: 10px;" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to clear all caches?', 'wpvulnerability' ) ); ?>');">
4160 <?php wp_nonce_field( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ); ?>
4161 <input type="hidden" name="wpvulnerability_debug_clear_caches" value="1">
4162 <button type="submit" class="button button-secondary">
4163 <?php esc_html_e( 'Clear All Caches', 'wpvulnerability' ); ?>
4164 </button>
4165 </form>
4166
4167 <form method="post" style="display: inline-block; margin-right: 10px;" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset signatures?', 'wpvulnerability' ) ); ?>');">
4168 <?php wp_nonce_field( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ); ?>
4169 <input type="hidden" name="wpvulnerability_debug_reset_signatures" value="1">
4170 <button type="submit" class="button button-secondary">
4171 <?php esc_html_e( 'Reset Signatures', 'wpvulnerability' ); ?>
4172 </button>
4173 </form>
4174
4175 <form method="post" style="display: inline-block;">
4176 <?php wp_nonce_field( 'wpvulnerability_debug_action', 'wpvulnerability_debug_nonce' ); ?>
4177 <input type="hidden" name="wpvulnerability_debug_export" value="1">
4178 <button type="submit" class="button button-primary">
4179 <?php esc_html_e( 'Export Debug Info', 'wpvulnerability' ); ?>
4180 </button>
4181 </form>
4182 </div>
4183 <?php else : ?>
4184 <p style="color: #d63638;">
4185 <?php esc_html_e( 'You do not have permission to perform these actions.', 'wpvulnerability' ); ?>
4186 </p>
4187 <?php endif; ?>
4188 </div>
4189 <?php
4190 }
4191