PluginProbe
Security Ninja – WordPress Security & Firewall / trunk
Security Ninja – WordPress Security & Firewall vtrunk
5.303 5.302 5.301 5.300 5.299 5.296 5.289 5.286 5.283 5.281 trunk 5.277 5.279
security-ninja / security-ninja.php

security-ninja.php in Security Ninja – WordPress Security & Firewall trunk, at security-ninja.php

2,139 lines 97.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 Plugin Name: Security Ninja
5 Plugin URI: https://wpsecurityninja.com/
6 Description: Check your site for security vulnerabilities and get precise suggestions for corrective actions on passwords, user accounts, file permissions, database security, version hiding, plugins, themes, security headers and other security aspects.
7 Author: WP Security Ninja
8 Version: 5.303
9 Author URI: https://wpsecurityninja.com/
10 License: GPLv3
11 License URI: https://www.gnu.org/licenses/gpl-3.0.html
12 Text Domain: security-ninja
13 Domain Path: /languages/mo
14
15 Copyright
16 2011-2019 Web Factory Ltd
17 2019- Larsik Corp
18
19 This program is free software; you can redistribute it and/or modify
20 it under the terms of the GNU General Public License as published by
21 the Free Software Foundation; either version 2 of the License, or
22 (at your option) any later version.
23
24 This program is distributed in the hope that it will be useful,
25 but WITHOUT ANY WARRANTY; without even the implied warranty of
26 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27 GNU General Public License for more details.
28
29 You should have received a copy of the GNU General Public License
30 along with this program; if not, write to the Free Software
31 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
32
33
34 This plugin uses the following 3rd party MIT licensed projects - Thank you for making other developer lives easier :-)
35
36 * Rename login module based on the WordPress Rename Login by Prathap Rathod.
37
38 * Country flags Copyright (c) 2017 Go Squared Ltd. http://www.gosquared.com/ - https://github.com/gosquared/flags. MIT license.
39
40 * PHP malware scanner - https://github.com/scr34m/php-malware-scanner
41 This plugin works on a modified version of the excellent PHP malware scanner.
42 */
43 namespace WPSecurityNinja\Plugin;
44
45 use Error;
46 use wf_sn_cf;
47 use Wf_Sn_Cs;
48 use Utils;
49 if ( !defined( 'ABSPATH' ) ) {
50 exit;
51 }
52 // Free (including older wp.org builds) may already be active: hand off to Freemius, do not load vendor again.
53 if ( function_exists( '\\WPSecurityNinja\\Plugin\\secnin_fs' ) ) {
54 secnin_fs()->set_basename( false, __FILE__ );
55 return;
56 }
57 if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
58 require_once __DIR__ . '/vendor/autoload.php';
59 }
60 if ( !function_exists( '\\fs_dynamic_init' ) && file_exists( __DIR__ . '/freemius/start.php' ) ) {
61 require_once __DIR__ . '/freemius/start.php';
62 }
63 if ( !function_exists( '\\fs_dynamic_init' ) ) {
64 return;
65 }
66 if ( !function_exists( '\\WPSecurityNinja\\Plugin\\secnin_fs' ) ) {
67 define( 'WF_SN_BASE_FILE', __FILE__ );
68 // Create a helper function for easy SDK access.
69 function secnin_fs() {
70 global $secnin_fs;
71 if ( !isset( $secnin_fs ) ) {
72 // Activate multisite network integration.
73 if ( !defined( 'WP_FS__PRODUCT_3690_MULTISITE' ) ) {
74 define( 'WP_FS__PRODUCT_3690_MULTISITE', true );
75 }
76 $secnin_fs = \fs_dynamic_init( array(
77 'id' => '3690',
78 'slug' => 'security-ninja',
79 'enable_anonymous' => true,
80 'type' => 'plugin',
81 'public_key' => 'pk_f990ec18700a90c02db544f1aa986',
82 'is_premium' => false,
83 'has_addons' => true,
84 'has_paid_plans' => true,
85 'trial' => array(
86 'days' => 14,
87 'is_require_payment' => false,
88 ),
89 'has_affiliation' => false,
90 'menu' => ( is_multisite() ? array(
91 'support' => false,
92 'network' => false,
93 'pricing' => false,
94 ) : array(
95 'slug' => 'wf-sn',
96 'first-path' => 'admin.php?page=security-ninja-wizard',
97 'support' => false,
98 'network' => false,
99 'pricing' => false,
100 ) ),
101 'is_live' => true,
102 'is_org_compliant' => true,
103 ) );
104 }
105 return $secnin_fs;
106 }
107
108 // Init Freemius.
109 secnin_fs();
110 // Signal that SDK was initiated.
111 do_action( 'secnin_fs_loaded' );
112 define( 'WF_SN_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
113 define( 'WF_SN_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
114 define( 'WF_SN_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
115 /**
116 * Whether this request is wp-admin, AJAX, cron, WP-CLI, import, or MainWP.
117 *
118 * @since 5.303
119 * @return bool
120 */
121 function secnin_is_admin_or_background_request() {
122 if ( is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
123 return true;
124 }
125 if ( defined( 'WP_CLI' ) && WP_CLI ) {
126 return true;
127 }
128 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
129 return true;
130 }
131 // MainWP child pings can look like frontend POSTs.
132 // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Detection only, not a form handler.
133 if ( isset( $_POST['mainwpsignature'] ) || isset( $_GET['mainwpsignature'] ) ) {
134 return true;
135 }
136 return false;
137 }
138
139 $secnin_load_admin_modules = secnin_is_admin_or_background_request();
140 include_once WF_SN_PLUGIN_DIR . 'modules/overview/class-wf-sn-overview-tab.php';
141 include_once WF_SN_PLUGIN_DIR . 'modules/dashboard-widget/class-wf-sn-dashboard-widget.php';
142 \WPSecurityNinja\Plugin\Wf_Sn_Dashboard_Widget::init();
143 include_once WF_SN_PLUGIN_DIR . 'modules/vulnerabilities/class-wf-sn-vu.php';
144 include_once WF_SN_PLUGIN_DIR . 'modules/core-scanner/core-scanner.php';
145 include_once WF_SN_PLUGIN_DIR . 'modules/file-viewer/class-fileviewer.php';
146 include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-wf-sn-cf-utils.php';
147 include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-wf-sn-security-utils.php';
148 include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/cloud-firewall.php';
149 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-utils.php';
150 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-test-descriptions.php';
151 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-free-render.php';
152 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-crypto.php';
153 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-admin-links.php';
154 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-security-snapshot.php';
155 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-security-snapshot-diff.php';
156 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-priority-resolver.php';
157 include_once WF_SN_PLUGIN_DIR . 'modules/events-logger/events-logger.php';
158 include_once WF_SN_PLUGIN_DIR . 'modules/wizard/class-wf-sn-wizard.php';
159 if ( apply_filters( 'wf_sn_ai_advisor_enabled', true ) ) {
160 include_once WF_SN_PLUGIN_DIR . 'modules/ai-security-advisor/class-wf-sn-ai-advisor.php';
161 add_action( 'init', array('\\WPSecurityNinja\\Plugin\\AiAdvisor\\Wf_Sn_Ai_Advisor', 'init') );
162 }
163 class Wf_Sn {
164 /**
165 * Plugin version
166 *
167 * @var integer
168 */
169 public static $version = null;
170
171 /**
172 * Cached dashboard test score payload.
173 *
174 * @var array<string, int|string>|null
175 */
176 public static $test_scores = null;
177
178 /**
179 * Plugin name
180 *
181 * @var string
182 */
183 public static $name = 'Security Ninja';
184
185 /**
186 * List of tests to skip
187 *
188 * @var array
189 */
190 public static $skip_tests = array();
191
192 /**
193 * Plugin options.
194 *
195 * @var array<string, mixed>
196 */
197 public static $options;
198
199 /**
200 * Database schema version; bump when table structure changes so dbDelta runs on upgrade.
201 *
202 * @var int
203 */
204 const WF_SN_DB_VERSION = 5.292;
205
206 /**
207 * Load plugin text domain for translations
208 *
209 * @author Lars Koudal
210 * @since v1.0.1
211 * @version v1.0.0 Tuesday, November 11th, 2025.
212 * @access public static
213 * @return void
214 */
215 public static function load_textdomain() {
216 load_plugin_textdomain( 'security-ninja', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/mo' );
217 }
218
219 /**
220 * Init the plugin
221 *
222 * @author Lars Koudal
223 * @since v0.0.1
224 * @version v1.0.0 Thursday, April 29th, 2021.
225 * @access public static
226 * @return void
227 */
228 public static function init() {
229 self::$options = self::get_options();
230 // loads persistent admin notices
231 add_action( 'admin_init', array('PAnD', 'init') );
232 include_once WF_SN_PLUGIN_DIR . 'class-wf-sn-tests.php';
233 include_once WF_SN_PLUGIN_DIR . 'includes/class-wf-sn-utils.php';
234 // MainWP integration - run here to make sure it's loaded
235 add_filter(
236 'mainwp_child_extra_execution',
237 array(__NAMESPACE__ . '\\Utils', 'do_filter_mainwp_child_extra_execution'),
238 10,
239 2
240 );
241 add_filter(
242 'mainwp_site_sync_others_data',
243 array(__NAMESPACE__ . '\\Utils', 'do_filter_mainwp_site_sync_others_data'),
244 10,
245 2
246 );
247 add_action( 'secnin_run_tests_event', array(__NAMESPACE__ . '\\Wf_Sn', 'do_event_run_tests') );
248 Wf_Sn_Security_Snapshot::register_hooks();
249 // Admin-ajax: register always; each callback enforces current_user_can( 'manage_options' ) (or stricter).
250 add_action( 'wp_ajax_sn_run_single_test', array(__NAMESPACE__ . '\\Wf_Sn', 'run_single_test') );
251 add_action( 'wp_ajax_sn_get_single_test_details', array(__NAMESPACE__ . '\\Wf_Sn', 'get_single_test_details') );
252 add_action( 'wp_ajax_sn_get_test_descriptions', array(__NAMESPACE__ . '\\Wf_Sn', 'get_test_descriptions') );
253 add_action( 'wp_ajax_sn_run_tests', array(__NAMESPACE__ . '\\Wf_Sn', 'run_tests') );
254 add_action( 'wp_ajax_sn_reset_secret_url', array(__NAMESPACE__ . '\\Wf_Sn', 'reset_secret_url') );
255 add_action( 'wp_ajax_wf_sn_dismiss_review', array(__NAMESPACE__ . '\\Wf_Sn', 'wf_sn_dismiss_review') );
256 // does the user have enough privilages to use the plugin?
257 if ( current_user_can( 'activate_plugins' ) ) {
258 // Adds extra permission to Freemius
259 if ( function_exists( '\\WPSecurityNinja\\Plugin\\secnin_fs' ) ) {
260 secnin_fs()->add_filter( 'permission_list', array(__NAMESPACE__ . '\\Utils', 'add_freemius_extra_permission') );
261 secnin_fs()->add_filter(
262 'show_admin_notice',
263 array(__NAMESPACE__ . '\\Utils', 'do_filter_show_admin_notice'),
264 10,
265 2
266 );
267 // Hide Add-Ons until Freemius account is registered (avoids broken Complementos/Add-Ons page).
268 secnin_fs()->add_filter(
269 'is_submenu_visible',
270 array(__NAMESPACE__ . '\\Utils', 'filter_freemius_addons_submenu_visibility'),
271 10,
272 2
273 );
274 secnin_fs()->add_filter( 'checkout/parameters', array(__NAMESPACE__ . '\\Utils', 'extra_modern_checkout_parameters') );
275 add_action( 'admin_init', array(__NAMESPACE__ . '\\Utils', 'secnin_fs_license_key_migration') );
276 secnin_fs()->add_filter( 'plugin_icon', array(__NAMESPACE__ . '\\Wf_Sn', 'secnin_fs_custom_icon') );
277 // Custom, security-focused opt-in message (Skip remains available).
278 secnin_fs()->add_filter(
279 'connect_message',
280 array(__NAMESPACE__ . '\\Utils', 'freemius_connect_message'),
281 10,
282 6
283 );
284 secnin_fs()->add_filter(
285 'connect_message_on_update',
286 array(__NAMESPACE__ . '\\Utils', 'freemius_connect_message'),
287 10,
288 6
289 );
290 // Free opt-in / Skip: land on Setup Wizard when setup is still incomplete.
291 secnin_fs()->add_filter( 'after_connect_url', array(__NAMESPACE__ . '\\Wf_Sn', 'filter_post_freemius_landing_url') );
292 secnin_fs()->add_filter( 'after_skip_url', array(__NAMESPACE__ . '\\Wf_Sn', 'filter_post_freemius_landing_url') );
293 }
294 add_filter(
295 'sn_tabs',
296 array(__NAMESPACE__ . '\\Wf_Sn', 'return_tabs'),
297 PHP_INT_MAX,
298 1
299 );
300 add_action( 'admin_menu', array(__NAMESPACE__ . '\\Wf_Sn', 'admin_menu') );
301 add_action(
302 'activated_plugin',
303 array(__NAMESPACE__ . '\\Wf_Sn', 'do_action_activated_plugin'),
304 10,
305 2
306 );
307 add_action( 'admin_enqueue_scripts', array(__NAMESPACE__ . '\\Wf_Sn', 'enqueue_scripts') );
308 add_action( 'admin_init', array(__NAMESPACE__ . '\\Wf_Sn', 'register_settings') );
309 add_action( 'admin_init', array(__NAMESPACE__ . '\\Wf_Sn', 'do_action_admin_init') );
310 add_action( 'admin_init', array(__NAMESPACE__ . '\\Wf_Sn', 'maybe_upgrade_db'), 1 );
311 add_action( 'admin_notices', array(__NAMESPACE__ . '\\Utils', 'do_admin_notices') );
312 add_action( 'admin_footer', array(__NAMESPACE__ . '\\Utils', 'admin_footer') );
313 add_action( 'secnin_signup_to_newsletter', array(__NAMESPACE__ . '\\Utils', 'signup_to_newsletter') );
314 add_filter( 'manage_users_columns', array(__NAMESPACE__ . '\\Utils', 'add_user_last_login_column') );
315 add_filter(
316 'manage_users_custom_column',
317 array(__NAMESPACE__ . '\\Utils', 'return_last_login_column'),
318 10,
319 3
320 );
321 }
322 }
323
324 /**
325 * Run create_tables_for_site (dbDelta) when stored DB version is older than WF_SN_DB_VERSION.
326 * Runs only in admin (admin_init) so the front-end is not hit. Set wf_sn_db_version in
327 * activate() after creating tables so the next request sees it and we avoid running twice.
328 */
329 public static function maybe_upgrade_db() {
330 $stored = (float) get_option( 'wf_sn_db_version', 0 );
331 if ( $stored >= self::WF_SN_DB_VERSION ) {
332 return;
333 }
334 global $wpdb;
335 include_once ABSPATH . 'wp-admin/includes/upgrade.php';
336 $charset = $wpdb->get_charset_collate();
337 \WPSecurityNinja\Plugin\Utils::create_tables_for_site( $charset );
338 update_option( 'wf_sn_db_version', self::WF_SN_DB_VERSION );
339 }
340
341 /**
342 * do_event_run_tests.
343 *
344 * @author Lars Koudal
345 * @since v0.0.1
346 * @version v1.0.0 Tuesday, December 12th, 2023.
347 * @version v1.0.1 Thursday, December 14th, 2023.
348 * @access public static
349 * @return void
350 */
351 public static function do_event_run_tests() {
352 $security_tests = wf_sn_tests::return_security_tests();
353 $resultssofar = get_option( 'security_tests_results', array() );
354 $set_time_limit = set_time_limit( 200 );
355 if ( !is_array( $security_tests ) || empty( $security_tests ) ) {
356 return;
357 }
358 $resultssofar['last_run'] = time();
359 foreach ( $security_tests as $test_name => $test ) {
360 $class_with_namespace = __NAMESPACE__ . '\\Wf_Sn_Tests';
361 if ( !method_exists( $class_with_namespace, $test_name ) ) {
362 continue;
363 }
364 // Start timer and call the test dynamically.
365 \WPSecurityNinja\Plugin\Utils::timerstart( 'run_test_' . esc_attr( $test_name ) );
366 $response = $class_with_namespace::$test_name();
367 if ( !is_array( $response ) || empty( $response ) ) {
368 continue;
369 }
370 // Setting appropriate message
371 if ( 10 === intval( $response['status'] ) ) {
372 $return_message = sprintf( (string) ($test['msg_ok'] ?? '%s'), $response['msg'] ?? '' );
373 } elseif ( 0 === intval( $response['status'] ) ) {
374 $return_message = sprintf( (string) ($test['msg_bad'] ?? '%s'), $response['msg'] ?? '' );
375 } else {
376 $return_message = sprintf( (string) ($test['msg_warning'] ?? '%s'), $response['msg'] ?? '' );
377 }
378 // Updates the results
379 $resultssofar['test'][$test_name] = array(
380 'title' => $test['title'],
381 'status' => $response['status'],
382 'score' => $test['score'],
383 'msg' => $return_message,
384 );
385 $end_time = \WPSecurityNinja\Plugin\Utils::timerstop( 'run_test_' . esc_attr( $test_name ) );
386 $testresult = array(
387 'testid' => $test_name,
388 'timestamp' => current_time( 'mysql' ),
389 'title' => $test['title'],
390 'status' => $response['status'],
391 'score' => $test['score'],
392 'runtime' => $end_time,
393 'msg' => $return_message,
394 'details' => '',
395 );
396 self::update_test_score( $testresult );
397 // Update the last test run
398 $resultssofar['last_test_run'] = $test_name;
399 update_option( 'security_tests_results', $resultssofar );
400 }
401 if ( class_exists( __NAMESPACE__ . '\\Wf_Sn_Cs' ) ) {
402 if ( method_exists( __NAMESPACE__ . '\\Wf_Sn_Cs', 'scan_files' ) ) {
403 \WPSecurityNinja\Plugin\wf_sn_el_modules::log_event( 'security_ninja', 'security_tests', 'Checking core files.' );
404 \WPSecurityNinja\Plugin\Wf_Sn_Cs::scan_files( true );
405 }
406 }
407 }
408
409 /**
410 * Redirects the user after plugin activation.
411 *
412 * @author Unknown
413 * @author Lars Koudal
414 * @since v0.0.1
415 * @version v1.0.0 Tuesday, February 22nd, 2022.
416 * @version v1.0.1 Saturday, March 5th, 2022.
417 * @access public static
418 * @return void
419 */
420 public static function do_action_admin_init() {
421 self::maybe_redirect_after_first_install();
422 // Check for secret URL reset success notice
423 $reset_success_url = get_transient( 'sn_secret_url_reset_success' );
424 if ( $reset_success_url ) {
425 delete_transient( 'sn_secret_url_reset_success' );
426 add_action( 'admin_notices', function () use($reset_success_url) {
427 echo '<div class="notice notice-success secnin-notice"><p>' . esc_html__( 'Secret access URL has been reset successfully.', 'security-ninja' ) . ' <strong>' . esc_html__( 'New URL:', 'security-ninja' ) . '</strong> <code>' . esc_url( $reset_success_url ) . '</code></p></div>';
428 } );
429 }
430 // Check for cleanup success notice.
431 if ( isset( $_GET['visitor_log_cleared'] ) && 'success' === sanitize_text_field( wp_unslash( $_GET['visitor_log_cleared'] ) ) ) {
432 add_action( 'admin_notices', function () {
433 echo '<div class="notice notice-success secnin-notice"><p>' . esc_html__( 'Firewall visitor log cleared successfully.', 'security-ninja' ) . '</p></div>';
434 } );
435 }
436 // Check for 2FA reset success notice.
437 if ( isset( $_GET['reset_2fa'] ) && 'success' === sanitize_text_field( wp_unslash( $_GET['reset_2fa'] ) ) ) {
438 add_action( 'admin_notices', function () {
439 echo '<div class="notice notice-success secnin-notice"><p>' . esc_html__( 'Two-factor authentication has been reset for all users.', 'security-ninja' ) . '</p></div>';
440 } );
441 }
442 }
443
444 /**
445 * Returns the one-time post-install/post-license landing page.
446 *
447 * @since 5.291
448 * @access private static
449 * @return string Admin URL.
450 */
451 private static function get_post_install_redirect_url() {
452 if ( class_exists( __NAMESPACE__ . '\\Wf_Sn_Wiz' ) && !\WPSecurityNinja\Plugin\Wf_Sn_Wiz::is_wizard_completed() ) {
453 return admin_url( 'admin.php?page=security-ninja-wizard' );
454 }
455 return admin_url( 'admin.php?page=wf-sn' );
456 }
457
458 /**
459 * Freemius after_connect_url / after_skip_url: prefer Setup Wizard when incomplete.
460 *
461 * @since 5.296
462 * @param string $url Default Freemius landing URL.
463 * @return string
464 */
465 public static function filter_post_freemius_landing_url( $url ) {
466 return self::get_post_install_redirect_url();
467 }
468
469 /**
470 * Redirects once after a true first install only.
471 *
472 * Lands on the main Security Ninja page so Freemius license entry is not
473 * skipped. The wizard is opened later via license-activation redirect when
474 * setup is still incomplete.
475 *
476 * The redirect flag is only set when wf_sn_options did not exist before
477 * activation, so updates and later reactivations of existing installs do
478 * not reopen the wizard.
479 *
480 * @since 5.291
481 * @access private static
482 * @return void
483 */
484 private static function maybe_redirect_after_first_install() {
485 if ( wp_doing_ajax() || is_network_admin() || !current_user_can( 'manage_options' ) ) {
486 return;
487 }
488 $redirect_user_id = absint( get_option( 'secnin_activation_redirect', 0 ) );
489 if ( !$redirect_user_id || get_current_user_id() !== $redirect_user_id ) {
490 return;
491 }
492 delete_option( 'secnin_activation_redirect' );
493 // Land on the main plugin page so Freemius license entry is not skipped.
494 wp_safe_redirect( admin_url( 'admin.php?page=wf-sn' ) );
495 exit;
496 }
497
498 /**
499 * do_action_activated_plugin.
500 *
501 * @author Lars Koudal
502 * @since v0.0.1
503 * @version v1.0.0 Saturday, March 6th, 2021.
504 * @access public static
505 * @param mixed $plugin
506 * @param mixed $network_wide
507 * @return void
508 */
509 public static function do_action_activated_plugin( $plugin, $network_wide ) {
510 // Bail if activating from network or bulk sites.
511 if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) {
512 return;
513 }
514 }
515
516 /**
517 * Handle new site creation in multisite.
518 *
519 * @author Lars Koudal
520 * @since v0.0.1
521 * @version v1.0.0 Tuesday, December 7th, 2021.
522 * @access public static
523 * @param int $site_id The site ID.
524 * @return void
525 */
526 public static function handle_new_site( $site_id ) {
527 if ( !is_multisite() ) {
528 return;
529 }
530 switch_to_blog( $site_id );
531 global $wpdb;
532 include_once ABSPATH . 'wp-admin/includes/upgrade.php';
533 $charset = $wpdb->get_charset_collate();
534 \WPSecurityNinja\Plugin\Utils::create_tables_for_site( $charset );
535 restore_current_blog();
536 }
537
538 /**
539 * Custom logo URL for Freemius dialogue
540 *
541 * @author Lars Koudal
542 * @since v0.0.1
543 * @version v1.0.0 Wednesday, January 13th, 2021.
544 * @access public static
545 * @return mixed
546 */
547 public static function secnin_fs_custom_icon() {
548 return __DIR__ . '/images/plugin-icon.png';
549 }
550
551 /**
552 * Update dismissed notice
553 *
554 * @author Lars Koudal
555 * @author Unknown
556 * @since v0.0.1
557 * @version v1.0.0 Tuesday, January 12th, 2021.
558 * @version v1.0.1 Friday, September 8th, 2023.
559 * @version v1.0.2 Thursday, November 30th, 2023.
560 * @access public static
561 * @return void
562 */
563 public static function wf_sn_dismiss_review() {
564 check_ajax_referer( 'wf_sn_dismiss_review' );
565 if ( !current_user_can( 'manage_options' ) ) {
566 wp_send_json_error( array(
567 'message' => 'Error saving dismiss pointer.',
568 ) );
569 wp_die();
570 }
571 $review = get_option( 'wf_sn_review_notice' );
572 if ( !$review ) {
573 $review = array();
574 }
575 $review['time'] = time() + WEEK_IN_SECONDS * 4;
576 $review['dismissed'] = true;
577 if ( isset( $_POST['user_data'] ) ) {
578 $review['signed_up'] = true;
579 }
580 update_option( 'wf_sn_review_notice', $review, false );
581 die;
582 }
583
584 /**
585 * Reorders plugin tabs: Events after Firewall, Pro tabs (Login, Scheduler, Malware, Whitelabel) at the end.
586 *
587 * @param array $tabs Array of tab definitions (each with 'id', etc.).
588 * @return array Reordered tabs.
589 */
590 public static function reorder_sn_tabs( $tabs ) {
591 if ( !is_array( $tabs ) || empty( $tabs ) ) {
592 return $tabs;
593 }
594 $pro_order = array(
595 'sn_login',
596 'sn_schedule',
597 'sn_malware',
598 'sn_whitelabel'
599 );
600 // 1. Move sn_logger to immediately after sn_cf.
601 $idx_cf = null;
602 $idx_logger = null;
603 foreach ( $tabs as $i => $tab ) {
604 if ( !empty( $tab['id'] ) ) {
605 if ( $tab['id'] === 'sn_cf' ) {
606 $idx_cf = $i;
607 }
608 if ( $tab['id'] === 'sn_logger' ) {
609 $idx_logger = $i;
610 }
611 }
612 }
613 if ( $idx_cf !== null && $idx_logger !== null && $idx_logger !== $idx_cf + 1 ) {
614 $logger_tab = $tabs[$idx_logger];
615 array_splice( $tabs, $idx_logger, 1 );
616 $insert_at = ( $idx_logger < $idx_cf ? $idx_cf : $idx_cf + 1 );
617 array_splice(
618 $tabs,
619 $insert_at,
620 0,
621 array($logger_tab)
622 );
623 }
624 // 2. Move Pro tabs to the end in order: Login, Scheduler, Malware, Whitelabel.
625 $pro_tabs = array();
626 foreach ( $pro_order as $id ) {
627 foreach ( $tabs as $tab ) {
628 if ( !empty( $tab['id'] ) && $tab['id'] === $id ) {
629 $pro_tabs[] = $tab;
630 break;
631 }
632 }
633 }
634 $tabs = array_values( array_filter( $tabs, function ( $tab ) use($pro_order) {
635 return empty( $tab['id'] ) || !in_array( $tab['id'], $pro_order, true );
636 } ) );
637 return array_merge( $tabs, $pro_tabs );
638 }
639
640 /**
641 * Prepares the tabs for the plugin interface
642 *
643 * @author Lars Koudal <me@larsik.com>
644 * @author Lars Koudal
645 * @since v0.0.1
646 * @version v1.0.0 Saturday, March 5th, 2022.
647 * @version v1.0.1 Friday, November 17th, 2023.
648 * @access public static
649 * @param mixed $intabs Array of tabs for plugin to be processed
650 * @return mixed
651 */
652 public static function return_tabs( $intabs ) {
653 $malware_tab = array(
654 'id' => 'sn_malware',
655 'class' => 'profeature',
656 'label' => esc_html__( 'Malware', 'security-ninja' ),
657 'callback' => array(__NAMESPACE__ . '\\Free_Render', 'render_malware_page'),
658 );
659 $schedule_tab = array(
660 'id' => 'sn_schedule',
661 'class' => 'profeature',
662 'label' => esc_html__( 'Scheduler', 'security-ninja' ),
663 'callback' => array(__NAMESPACE__ . '\\Free_Render', 'render_scheduled_scanner_page'),
664 );
665 $whitelabel_tab = array(
666 'id' => 'sn_whitelabel',
667 'class' => 'profeature',
668 'label' => esc_html__( 'White label', 'security-ninja' ),
669 'callback' => array(__NAMESPACE__ . '\\Utils', 'render_whitelabel_page'),
670 );
671 $login_tab = array(
672 'id' => 'sn_login',
673 'class' => 'profeature',
674 'label' => esc_html__( 'Login', 'security-ninja' ),
675 'callback' => array(__NAMESPACE__ . '\\wf_sn_cf', 'do_login_page'),
676 );
677 $outtabs = $intabs;
678 $login_inserted = false;
679 foreach ( $outtabs as $idx => $tab ) {
680 if ( !empty( $tab['id'] ) && $tab['id'] === 'sn_cf' ) {
681 $has_login = false;
682 foreach ( $outtabs as $t ) {
683 if ( !empty( $t['id'] ) && $t['id'] === 'sn_login' ) {
684 $has_login = true;
685 break;
686 }
687 }
688 if ( !$has_login ) {
689 array_splice(
690 $outtabs,
691 $idx + 1,
692 0,
693 array($login_tab)
694 );
695 }
696 $login_inserted = true;
697 break;
698 }
699 }
700 if ( !$login_inserted ) {
701 $has_login = false;
702 foreach ( $outtabs as $t ) {
703 if ( !empty( $t['id'] ) && $t['id'] === 'sn_login' ) {
704 $has_login = true;
705 break;
706 }
707 }
708 if ( !$has_login ) {
709 $outtabs[] = $login_tab;
710 }
711 }
712 $outtabs[] = $schedule_tab;
713 $outtabs[] = $malware_tab;
714 $outtabs[] = $whitelabel_tab;
715 $outtabs = self::reorder_sn_tabs( $outtabs );
716 return $outtabs;
717 }
718
719 /**
720 * Checks if the current page is a part of this plugin.
721 *
722 * @author Lars Koudal
723 * @since v0.0.1
724 * @version v1.0.0 Wednesday, January 13th, 2021.
725 * @access public static
726 * @return bool Returns true if the current page is a part of this plugin, otherwise false.
727 */
728 public static function is_plugin_page() {
729 $current_screen = get_current_screen();
730 if ( !$current_screen ) {
731 return false;
732 }
733 // Get the current screen ID
734 $current_id = $current_screen->id;
735 // Extract the page part after the last underscore
736 $page_part = substr( $current_id, strrpos( $current_id, '_' ) + 1 );
737 // Define our plugin pages using just the page part
738 $plugin_pages = array();
739 $plugin_pages[] = 'wf-sn';
740 $plugin_pages[] = 'security-ninja-welcome';
741 $plugin_pages[] = 'wf-sn-overview';
742 $plugin_pages[] = 'wf-sn-firewall';
743 $plugin_pages[] = 'wf-sn-scanner';
744 $plugin_pages[] = 'wf-sn-events';
745 $plugin_pages[] = 'wf-sn-advisor';
746 $plugin_pages[] = 'wf-sn-settings';
747 $plugin_pages[] = 'security-ninja-wizard';
748 // Check if the page part matches any of our plugin pages
749 return in_array( $page_part, $plugin_pages, true );
750 }
751
752 /**
753 * Register and enqueue shared SnDialog assets (free + Pro).
754 *
755 * Safe to call from any admin or front-end surface that needs dialogs.
756 *
757 * @since 5.292
758 * @return void
759 */
760 public static function enqueue_sn_dialog() {
761 wp_enqueue_style(
762 'sn-dialog',
763 WF_SN_PLUGIN_URL . 'css/sn-dialog.css',
764 array(),
765 filemtime( WF_SN_PLUGIN_DIR . 'css/sn-dialog.css' )
766 );
767 if ( !wp_script_is( 'sn-dialog', 'registered' ) ) {
768 wp_register_script(
769 'sn-dialog',
770 WF_SN_PLUGIN_URL . 'js/sn-dialog.js',
771 array(),
772 filemtime( WF_SN_PLUGIN_DIR . 'js/sn-dialog.js' ),
773 true
774 );
775 wp_localize_script( 'sn-dialog', 'snDialogL10n', array(
776 'ok' => esc_html__( 'OK', 'security-ninja' ),
777 'cancel' => esc_html__( 'Cancel', 'security-ninja' ),
778 'close' => esc_html__( 'Close', 'security-ninja' ),
779 ) );
780 }
781 wp_enqueue_script( 'sn-dialog' );
782 }
783
784 /**
785 * Enqueue CSS and JS scripts on plugin's pages
786 *
787 * @author Lars Koudal
788 * @author Unknown
789 * @since v0.0.1
790 * @version v1.0.0 Wednesday, January 13th, 2021.
791 * @version v1.0.1 Sunday, May 11th, 2025.
792 * @access public static
793 * @param mixed $hook
794 * @return void
795 */
796 public static function enqueue_scripts( $hook ) {
797 if ( 'wp-admin/update.php' === $GLOBALS['pagenow'] ) {
798 return;
799 }
800 if ( self::is_plugin_page() ) {
801 $sn_global_deps = array('jquery', 'jquery-ui-tabs');
802 wp_enqueue_script( 'jquery-ui-tabs' );
803 wp_enqueue_script(
804 'sn-global',
805 WF_SN_PLUGIN_URL . 'js/sn-global.js',
806 $sn_global_deps,
807 filemtime( WF_SN_PLUGIN_DIR . 'js/sn-global.js' ),
808 true
809 );
810 wp_enqueue_script(
811 'sn-jquery-plugins',
812 WF_SN_PLUGIN_URL . 'js/sn-jquery-plugins.js',
813 array('jquery'),
814 filemtime( WF_SN_PLUGIN_DIR . 'js/sn-jquery-plugins.js' ),
815 true
816 );
817 self::enqueue_sn_dialog();
818 $is_registered = false;
819 if ( secnin_fs()->is_registered() ) {
820 $is_registered = true;
821 }
822 $is_tracking_allowed = false;
823 if ( secnin_fs()->is_tracking_allowed() ) {
824 $is_tracking_allowed = true;
825 }
826 if ( $is_registered && $is_tracking_allowed ) {
827 // Checks if user opted-in (or activated a license) and didn't opt out from tracking.
828 wp_enqueue_script(
829 'security-ninja-widget-sdk',
830 'https://securityninja.productlift.dev/widgets_sdk',
831 array(),
832 \WPSecurityNinja\Plugin\Utils::get_plugin_version(),
833 // Add version to prevent caching issues
834 true
835 );
836 }
837 // sn-common.js and wf_sn localization on all plugin pages (tests, dialogs, etc.). ProductLift stays gated above.
838 wp_register_script(
839 'sn-js',
840 WF_SN_PLUGIN_URL . 'js/sn-common.js',
841 array('jquery', 'wp-i18n', 'sn-dialog'),
842 filemtime( WF_SN_PLUGIN_DIR . 'js/sn-common.js' ),
843 true
844 );
845 $js_vars = array(
846 'sn_plugin_url' => WF_SN_PLUGIN_URL,
847 'nonce_run_tests' => wp_create_nonce( 'wf_sn_run_tests' ),
848 'nonce_refresh_update' => wp_create_nonce( 'wf_sn_refresh_update' ),
849 'nonce_dismiss_review' => wp_create_nonce( 'wf_sn_dismiss_review' ),
850 'nonce_reset_activation' => wp_create_nonce( 'wf_sn_reset_activation' ),
851 'nonce_latest_events' => wp_create_nonce( 'wf_sn_latest_events' ),
852 'nonce_reset_secret_url' => wp_create_nonce( 'wf-sn-reset-secret-url' ),
853 'lc_version' => \WPSecurityNinja\Plugin\Utils::get_plugin_version(),
854 'lc_site' => get_home_url(),
855 'lc_ip' => $_SERVER['REMOTE_ADDR'],
856 'strings' => array(
857 'reset_secret_url_confirm' => esc_html__( 'Are you sure you want to reset the secret access URL? The old link will no longer function.', 'security-ninja' ),
858 'resetting' => esc_html__( 'Resetting...', 'security-ninja' ),
859 'resetting_message' => esc_html__( 'Resetting secret access URL...', 'security-ninja' ),
860 'reset_button_text' => esc_html__( 'Reset Secret Access URL', 'security-ninja' ),
861 'error_unknown' => esc_html__( 'Unknown error occurred', 'security-ninja' ),
862 'error_failed' => esc_html__( 'Failed to reset secret access URL. Please try again.', 'security-ninja' ),
863 ),
864 );
865 wp_localize_script( 'sn-js', 'wf_sn', $js_vars );
866 wp_enqueue_script( 'sn-js' );
867 wp_enqueue_style(
868 'sn-css',
869 WF_SN_PLUGIN_URL . 'css/sn-style.css',
870 array(),
871 filemtime( WF_SN_PLUGIN_DIR . 'css/sn-style.css' )
872 );
873 // Removing scripts and styles from other plugins we know mess up the interface
874 wp_dequeue_style( 'uiStyleSheet' );
875 wp_dequeue_style( 'wpcufpnAdmin' );
876 wp_dequeue_style( 'unifStyleSheet' );
877 wp_dequeue_style( 'wpcufpn_codemirror' );
878 wp_dequeue_style( 'wpcufpn_codemirrorTheme' );
879 wp_dequeue_style( 'collapse-admin-css' );
880 wp_dequeue_style( 'jquery-ui-css' );
881 wp_dequeue_style( 'tribe-common-admin' );
882 wp_dequeue_style( 'file-manager__jquery-ui-css' );
883 wp_dequeue_style( 'file-manager__jquery-ui-css-theme' );
884 wp_dequeue_style( 'wpmegmaps-jqueryui' );
885 wp_dequeue_style( 'facebook-plugin-css' );
886 wp_dequeue_style( 'facebook-tip-plugin-css' );
887 wp_dequeue_style( 'facebook-member-plugin-css' );
888 wp_dequeue_style( 'kc-testimonial-admin' );
889 wp_dequeue_style( 'jquery-ui-style' );
890 }
891 }
892
893 /**
894 * add entry to admin menu
895 *
896 * @author Unknown
897 * @since v0.0.1
898 * @version v1.0.0 Friday, February 5th, 2021.
899 * @access public static
900 * @return void
901 */
902 public static function admin_menu() {
903 // Define menu constants
904 $page_title = 'Security';
905 $menu_title = 'Security Ninja';
906 $capability = 'manage_options';
907 $menu_slug = 'wf-sn';
908 $icon_url = \WPSecurityNinja\Plugin\Utils::get_icon_svg( true );
909 // Add notification count if needed
910 $notification_count = false;
911 if ( class_exists( __NAMESPACE__ . '\\Wf_Sn_Vu' ) ) {
912 $vu_options = wf_sn_vu::get_options();
913 if ( $vu_options['enable_admin_notification'] ) {
914 try {
915 $notification_count = Wf_Sn_Vu::return_vuln_count();
916 } catch ( \Exception $e ) {
917 $notification_count = false;
918 }
919 }
920 }
921 // Register main menu only if it doesn't exist
922 if ( !menu_page_url( $menu_slug, false ) ) {
923 add_menu_page(
924 $page_title,
925 ( $notification_count ? sprintf( $menu_title . ' <span class="awaiting-mod">%d</span>', $notification_count ) : $menu_title ),
926 $capability,
927 $menu_slug,
928 array(__NAMESPACE__ . '\\wf_sn', 'main_page'),
929 $icon_url
930 );
931 }
932 // Register submenus only for pro users
933 $add_advisor_for_free = true;
934 if ( $add_advisor_for_free && apply_filters( 'wf_sn_ai_advisor_enabled', true ) ) {
935 // Free build: only Security Advisor submenu (premium block was stripped)
936 add_submenu_page(
937 $menu_slug,
938 __( 'Security Advisor', 'security-ninja' ),
939 __( 'Security Advisor', 'security-ninja' ),
940 $capability,
941 'wf-sn-advisor',
942 array('\\WPSecurityNinja\\Plugin\\AiAdvisor\\Wf_Sn_Ai_Advisor', 'render_page')
943 );
944 }
945 $wizard_menu_label = ( \WPSecurityNinja\Plugin\Wf_Sn_Wiz::is_wizard_completed() ? __( 'Setup Wizard', 'security-ninja' ) : __( 'Get started', 'security-ninja' ) );
946 add_submenu_page(
947 $menu_slug,
948 __( 'Setup Wizard', 'security-ninja' ),
949 $wizard_menu_label,
950 $capability,
951 'security-ninja-wizard',
952 array(__NAMESPACE__ . '\\Wf_Sn_Wiz', 'wizard_page')
953 );
954 // Append AI badge to Security Advisor menu item (all users when advisor is shown)
955 if ( apply_filters( 'wf_sn_ai_advisor_enabled', true ) ) {
956 add_filter( 'admin_menu', array(__CLASS__, 'add_advisor_menu_badge'), 999 );
957 add_action( 'admin_enqueue_scripts', array(__CLASS__, 'enqueue_advisor_menu_badge_style'), 10 );
958 }
959 }
960
961 /**
962 * Appends the "AI" badge to the Security Advisor submenu title.
963 *
964 * @since 5.273
965 */
966 public static function add_advisor_menu_badge() {
967 global $submenu;
968 $parent = 'wf-sn';
969 if ( empty( $submenu[$parent] ) || !is_array( $submenu[$parent] ) ) {
970 return;
971 }
972 foreach ( $submenu[$parent] as $key => $item ) {
973 if ( isset( $item[2] ) && $item[2] === 'wf-sn-advisor' ) {
974 $submenu[$parent][$key][0] = $item[0] . ' <span class="sn-ai-menu-badge">AI</span>';
975 break;
976 }
977 }
978 }
979
980 /**
981 * Outputs inline CSS for the Security Advisor menu badge on all admin pages.
982 *
983 * @since 5.273
984 */
985 public static function enqueue_advisor_menu_badge_style() {
986 wp_enqueue_style( 'dashicons' );
987 $css = '.sn-ai-menu-badge{display:inline-block;margin-left:4px;padding:2px 6px;font-size:10px;font-weight:600;line-height:1.2;color:#1d2327;background:#dcdcde;border-radius:3px;vertical-align:middle}';
988 wp_add_inline_style( 'dashicons', $css );
989 }
990
991 /**
992 * Add an error to the settings_error
993 *
994 * @author Lars Koudal
995 * @since v0.0.1
996 * @version v1.0.0 Thursday, January 14th, 2021.
997 * @access public static
998 * @param mixed $message
999 * @param string $type Default: 'error'
1000 * @param string $code Default: 'wf_sn'
1001 * @return void
1002 */
1003 public static function add_settings_error( $message, $type = 'error', $code = 'wf_sn' ) {
1004 global $wp_settings_errors;
1005 $new_wp_settings = $wp_settings_errors;
1006 $new_wp_settings[] = array(
1007 'setting' => 'wf_sn_options',
1008 'code' => $code,
1009 'message' => $message,
1010 'type' => $type,
1011 );
1012 set_transient( 'settings_errors', $new_wp_settings );
1013 }
1014
1015 /**
1016 * return default options
1017 *
1018 * @author Lars Koudal
1019 * @since v0.0.1
1020 * @version v1.0.0 Thursday, January 14th, 2021.
1021 * @access public static
1022 * @return mixed
1023 */
1024 public static function default_options() {
1025 $defaults = array(
1026 'license_key' => '',
1027 'license_active' => false,
1028 'license_expires' => '',
1029 'license_type' => '',
1030 'license_hide' => false,
1031 'first_version' => '',
1032 'first_install' => '',
1033 'wizard_completed_at' => '',
1034 'remove_settings_deactivate' => 0,
1035 );
1036 return $defaults;
1037 }
1038
1039 /**
1040 * get plugin's options
1041 *
1042 * @author Lars Koudal
1043 * @since v0.0.1
1044 * @version v1.0.0 Thursday, January 14th, 2021.
1045 * @access public static
1046 * @return mixed
1047 */
1048 public static function get_options() {
1049 if ( isset( self::$options ) && 0 < count( self::$options ) ) {
1050 return self::$options;
1051 }
1052 $options = get_option( 'wf_sn_options', array() );
1053 if ( isset( $options[0] ) ) {
1054 unset($options[0]);
1055 }
1056 if ( !is_array( $options ) ) {
1057 $options = array();
1058 }
1059 $options = array_merge( self::default_options(), $options );
1060 self::$options = $options;
1061 // her sætter vi globale options.
1062 return $options;
1063 }
1064
1065 /**
1066 * all settings are saved in one option
1067 *
1068 * @author Lars Koudal
1069 * @since v0.0.1
1070 * @version v1.0.0 Thursday, January 14th, 2021.
1071 * @access public static
1072 * @return void
1073 */
1074 public static function register_settings() {
1075 register_setting( 'wf_sn_options', 'wf_sn_options', array(__NAMESPACE__ . '\\wf_sn', 'sanitize_settings') );
1076 // we do not want to redirect everyone
1077 $redirect_user = false;
1078 if ( isset( $_POST['foo'], $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'wf-sn-install-routines' ) ) {
1079 if ( !current_user_can( 'manage_options' ) ) {
1080 wp_send_json_error( array(
1081 'success' => false,
1082 'message' => esc_html__( 'You do not have permission to do this.', 'security-ninja' ),
1083 ) );
1084 }
1085 $redirect_user = true;
1086 }
1087 if ( $redirect_user ) {
1088 // Set to false per default, so isset check not needed.
1089 if ( !isset( $_POST['_wp_http_referer'] ) ) {
1090 $_POST['_wp_http_referer'] = wp_login_url();
1091 }
1092 $url = sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) );
1093 wp_safe_redirect( urldecode( $url ) );
1094 exit;
1095 }
1096 }
1097
1098 /**
1099 * Sanitize settings on save
1100 *
1101 * @author Lars Koudal
1102 * @since v0.0.1
1103 * @version v1.0.0 Tuesday, January 12th, 2021.
1104 * @access public static
1105 * @param mixed $new_values
1106 * @return void
1107 */
1108 public static function sanitize_settings( $new_values ) {
1109 // Get the raw options from database without merging with defaults
1110 $old_options = get_option( 'wf_sn_options', array() );
1111 // Only merge with defaults if the options are actually empty or corrupted
1112 if ( empty( $old_options ) || !is_array( $old_options ) ) {
1113 $old_options = self::default_options();
1114 }
1115 $old_options['remove_settings_deactivate'] = 0;
1116 if ( !is_array( $new_values ) ) {
1117 $arr = array();
1118 $arr[] = $new_values;
1119 $new_values = $arr;
1120 }
1121 foreach ( $new_values as $key => $value ) {
1122 $new_values[$key] = sanitize_text_field( $value );
1123 }
1124 return array_merge( $old_options, $new_values );
1125 }
1126
1127 /**
1128 * whole options page
1129 *
1130 * @author Lars Koudal
1131 * @since v0.0.1
1132 * @version v1.0.0 Thursday, January 14th, 2021.
1133 * @access public static
1134 * @return void
1135 */
1136 public static function main_page() {
1137 global $secnin_fs;
1138 // Display setting errors
1139 settings_errors();
1140 $tabs = array();
1141 $tabs[] = array(
1142 'id' => 'sn_overview',
1143 'class' => '',
1144 'label' => __( 'Overview', 'security-ninja' ),
1145 'callback' => array(__NAMESPACE__ . '\\WF_SN_Overview_Tab', 'tab_overview'),
1146 );
1147 $tabs[] = array(
1148 'id' => 'sn_tests',
1149 'class' => '',
1150 'label' => __( 'Tests', 'security-ninja' ),
1151 'callback' => array(__NAMESPACE__ . '\\wf_sn', 'tab_tests'),
1152 );
1153 $tabs = apply_filters( 'sn_tabs', $tabs );
1154 ?>
1155 <div class="wrap">
1156 <?php
1157 \WPSecurityNinja\Plugin\Utils::show_topbar();
1158 ?>
1159 <div class="secnin_content_wrapper">
1160 <div class="secnin_content_cell" id="secnin_content_top">
1161
1162 <?php
1163 do_action( 'secnin_signup_to_newsletter' );
1164 ?>
1165
1166 <div class="nav-tab-wrapper" id="wf-sn-tabs">
1167 <?php
1168 foreach ( $tabs as $tab ) {
1169 $extra = '';
1170 $class = 'nav-tab ' . $tab['class'];
1171 if ( 'sn_overview' === $tab['id'] ) {
1172 $class .= ' nav-tab-active';
1173 }
1174 if ( !empty( $tab['label'] ) ) {
1175 if ( isset( $tab['count'] ) ) {
1176 $extra = ' <span class="warn-count">' . intval( $tab['count'] ) . '</span>';
1177 }
1178 echo '<a href="#' . esc_attr( $tab['id'] ) . '" class="' . esc_attr( $class ) . '" id="' . esc_attr( $tab['id'] ) . '-tab">' . esc_html( $tab['label'] ) . wp_kses( $extra, array(
1179 'span' => array(
1180 'class' => array(),
1181 ),
1182 ) ) . '</a>';
1183 }
1184 }
1185 ?>
1186 </div>
1187 <div id="sn_tabscont">
1188 <?php
1189 foreach ( $tabs as $tab ) {
1190 if ( !empty( $tab['callback'] ) ) {
1191 $class = 'wf-sn-tab';
1192 if ( 'sn_overview' === $tab['id'] ) {
1193 $class .= ' active';
1194 }
1195 echo '<div id="' . esc_attr( $tab['id'] ) . '" class="' . esc_attr( $class ) . '">';
1196 call_user_func( $tab['callback'] );
1197 echo '</div>';
1198 }
1199 }
1200 ?>
1201 </div>
1202 <?php
1203 include_once 'includes/sidebar.php';
1204 ?>
1205 </div>
1206 </div>
1207 </div>
1208
1209 <?php
1210 }
1211
1212 /**
1213 * returns the current score of the tests + output
1214 *
1215 * @author Lars Koudal
1216 * @since v0.0.1
1217 * @version v1.0.0 Thursday, January 14th, 2021.
1218 * @access public static
1219 * @return mixed
1220 */
1221 public static function return_test_scores() {
1222 if ( !is_null( self::$test_scores ) ) {
1223 return self::$test_scores;
1224 }
1225 global $wpdb;
1226 $testsresults = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wf_sn_tests LIMIT 100;", ARRAY_A );
1227 $bad = 0;
1228 $warning = 0;
1229 $good = 0;
1230 $score = 0;
1231 $total = 0;
1232 if ( $testsresults ) {
1233 foreach ( $testsresults as $test_details ) {
1234 $total += $test_details['score'];
1235 if ( 10 === intval( $test_details['status'] ) ) {
1236 ++$good;
1237 $score += $test_details['score'];
1238 } elseif ( 0 === intval( $test_details['status'] ) ) {
1239 ++$bad;
1240 } else {
1241 ++$warning;
1242 }
1243 }
1244 }
1245 if ( $total > 0 && $score > 0 ) {
1246 $score = round( $score / $total * 100 );
1247 } else {
1248 $score = 0;
1249 }
1250 $response = array();
1251 $response['good'] = $good;
1252 $response['bad'] = $bad;
1253 $response['warning'] = $warning;
1254 $response['score'] = $score;
1255 // generate output
1256 $output = '';
1257 $output .= '<div id="counters">';
1258 $output .= '<span class="edge good"><span class="val">' . $good . '</span><i>' . __( 'Passed', 'security-ninja' ) . '</i></span>';
1259 $output .= '<span class="edge warning"><span class="val">' . $warning . '</span><i>' . __( 'Warnings', 'security-ninja' ) . '</i></span>';
1260 $output .= '<span class="edge bad"><span class="val">' . $bad . '</span><i>' . __( 'Failed', 'security-ninja' ) . '</i></span>';
1261 $output .= '<span class="edge score"><span class="val">' . $score . '%</span><i>' . __( 'Score', 'security-ninja' ) . '</i></span>';
1262 $output .= '</div>';
1263 $response['output'] = $output;
1264 self::$test_scores = $response;
1265 return $response;
1266 }
1267
1268 /**
1269 * Gets test results from database
1270 *
1271 * @author Lars Koudal
1272 * @since v0.0.1
1273 * @version v1.0.0 Thursday, January 14th, 2021.
1274 * @access public static
1275 * @return mixed
1276 */
1277 public static function get_test_results() {
1278 global $wpdb;
1279 $table_name = $wpdb->prefix . 'wf_sn_tests';
1280 $testsresults = $wpdb->get_results( "SELECT * FROM {$table_name}", ARRAY_A );
1281 if ( !$testsresults ) {
1282 return false;
1283 }
1284 $response = array();
1285 foreach ( $testsresults as $tr ) {
1286 $response['test'][$tr['testid']] = $tr;
1287 }
1288 return $response;
1289 }
1290
1291 /**
1292 * tab_tests.
1293 *
1294 * @author Lars Koudal
1295 * @since v0.0.1
1296 * @version v1.0.0 Wednesday, February 3rd, 2021.
1297 * @access public static
1298 * @return void
1299 */
1300 public static function tab_tests() {
1301 $testsresults = self::get_test_results();
1302 ?>
1303 <div class="submit-test-container sncard settings-card">
1304 <h2><span class="dashicons dashicons-list-view"></span>
1305 <?php
1306 esc_html_e( 'Test your website security', 'security-ninja' );
1307 ?>
1308 </h2>
1309 <p class="description">
1310 <?php
1311 esc_html_e( 'Run comprehensive security tests to identify potential vulnerabilities and strengthen your website\'s defenses', 'security-ninja' );
1312 ?>
1313 </p>
1314 <div class="testresults" id="testscores">
1315 <?php
1316 $scores = self::return_test_scores();
1317 if ( isset( $scores['output'] ) ) {
1318 $allowed_html = array(
1319 'div' => array(
1320 'id' => array(),
1321 ),
1322 'span' => array(
1323 'class' => array(),
1324 ),
1325 'br' => array(),
1326 'i' => array(),
1327 );
1328 echo wp_kses( $scores['output'], $allowed_html );
1329 }
1330 ?>
1331 </div>
1332
1333 <?php
1334 $tests = wf_sn_tests::return_security_tests();
1335 $out = '<div id="runtestsrow"><button type="button" id="run-selected-tests" class="button button-primary button-hero wf-sn-button button-center">' . esc_html__( 'Run Tests', 'security-ninja' ) . '</button>';
1336 $out .= '<span class="runtestsbn spinner"></span>';
1337 $out .= '<div id="secninja-tests-quickselect">';
1338 $out .= '<span>' . __( 'Quick Filter', 'security-ninja' ) . ':</span><ul><li><a href="#" id="sn-quickselect-all">' . __( 'All', 'security-ninja' ) . '</a></li><li><a href="#" id="sn-quickselect-failed">' . __( 'Failed', 'security-ninja' ) . '</a></li><li><a href="#" id="sn-quickselect-warning">' . __( 'Warning', 'security-ninja' ) . '</a></li><li><a href="#" id="sn-quickselect-okay">' . __( 'Passed', 'security-ninja' ) . '</a></li><li><a href="#" id="sn-quickselect-untested">' . __( 'Untested', 'security-ninja' ) . '</a></li>';
1339 $out .= '</ul>';
1340 $out .= ' <span class="secnin-expand-all-wrap">| <a href="#" class="secnin_expand_all_details">' . esc_html__( 'Expand all details', 'security-ninja' ) . '</a></span>';
1341 $out .= '</div></div>';
1342 $out .= '<table class="wp-list-table widefat" cellspacing="0" id="security-ninja">';
1343 $out .= '<thead><tr>';
1344 $out .= '<td id="cb" class="manage-column column-cb check-column">';
1345 $out .= '<label class="screen-reader-text" for="cb-select-all-1">' . __( 'Select All', 'security-ninja' ) . '</label>';
1346 $out .= '<input id="cb-select-all-1" type="checkbox"></td>';
1347 $out .= '<th>' . __( 'Status', 'security-ninja' ) . '</th>';
1348 $out .= '<th class="column-primary">' . __( 'Security Test', 'security-ninja' ) . '</th>';
1349 $out .= '<th>' . __( 'Actions', 'security-ninja' );
1350 $out .= '</th>';
1351 $out .= '</tr></thead>';
1352 $out .= '<tbody>';
1353 if ( is_array( $tests ) ) {
1354 $stepid = 0;
1355 foreach ( $tests as $test_name => $details ) {
1356 if ( 'ad_' === substr( $test_name, 0, 3 ) || '_' === $test_name[0] ) {
1357 continue;
1358 }
1359 ++$stepid;
1360 $outlabel = '';
1361 $fixable_row = '';
1362 // hvis vi har kørt testen før
1363 if ( isset( $testsresults['test'][$test_name]['status'] ) ) {
1364 $out .= '<tr class="wf-sn-test-row-status-' . $testsresults['test'][$test_name]['status'] . ' test test_' . $test_name . $fixable_row . '">';
1365 if ( 0 === intval( $testsresults['test'][$test_name]['status'] ) ) {
1366 $outlabel = '<span class="teststatus fail">' . __( '', 'security-ninja' ) . '</span>';
1367 } elseif ( 5 === intval( $testsresults['test'][$test_name]['status'] ) ) {
1368 $outlabel = '<span class="teststatus warning">' . __( '-;', 'security-ninja' ) . '</span>';
1369 } elseif ( 10 === intval( $testsresults['test'][$test_name]['status'] ) ) {
1370 $outlabel = '<span class="teststatus pass">' . __( '', 'security-ninja' ) . '</span>';
1371 }
1372 } else {
1373 // lars - kommenteret ud ellers kom der er et "d" med
1374 $out .= '<tr class="wf-sn-test-row-status-null test test_' . $test_name . $fixable_row . '">';
1375 $outlabel = '<span class="teststatus untested">' . __( 'Untested', 'security-ninja' ) . '</span>';
1376 }
1377 $checkedoutput = checked( true, true, false );
1378 if ( !isset( $options['run_tests'] ) ) {
1379 $checkedoutput = checked( true, true, false );
1380 } else {
1381 $options = self::get_options();
1382 if ( in_array( $test_name, $options['run_tests'], true ) ) {
1383 $checkedoutput = checked( true, true, false );
1384 } else {
1385 $checkedoutput = checked( false, true, false );
1386 }
1387 }
1388 $out .= '<th scope="row" class="check-column"><input id="cb-select-' . $stepid . '" type="checkbox" name="sntest[]" value="' . sanitize_key( $test_name ) . '" ' . $checkedoutput . '/></th>';
1389 $out .= '<td scope="row" class="">' . $outlabel . '</td>';
1390 $out .= '<td class="column-primary" data-colname="Test"><label for="cb-select-' . $stepid . '"><span class="wf-sn-test-title">' . $details['title'] . '</span></label></br>';
1391 if ( isset( $testsresults['test'][$test_name]['msg'] ) ) {
1392 // only add details if failed or warning
1393 $outmessage = $testsresults['test'][$test_name]['msg'];
1394 // Add the details if exists
1395 if ( $testsresults['test'][$test_name]['details'] ) {
1396 $outmessage .= ' ' . $testsresults['test'][$test_name]['details'];
1397 }
1398 $out .= '<span class="sn-result-details">' . $outmessage . '</span>';
1399 } else {
1400 // empty - can be filled via ajax response
1401 $out .= '<span class="sn-result-details"></span>';
1402 }
1403 $out .= '<button type="button" class="toggle-row"> <span class="screen-reader-text">' . __( 'Show details', 'security-ninja' ) . '</span> </button>';
1404 $testcssid = 'tdesc-test-id-' . $test_name;
1405 $out .= '<div class="testdesc ' . esc_attr( $testcssid ) . '"></div>';
1406 if ( class_exists( __NAMESPACE__ . '\\wf_sn_af_fix_' . $test_name ) && isset( $details['status'] ) && 10 !== $details['status'] ) {
1407 $details_label = __( 'Details &amp; Fix', 'security-ninja' );
1408 } else {
1409 $details_label = __( 'Details', 'security-ninja' );
1410 }
1411 $out .= '<div class="testtimedetails ' . esc_attr( $test_name ) . '"><span class="spinner"></span><span class="lasttest"></span><span class="runtime"></span><span class="score"></span>';
1412 $outstatus = '';
1413 if ( isset( $testsresults['test'][$test_name]['status'] ) ) {
1414 $outstatus = $testsresults['test'][$test_name]['status'];
1415 }
1416 $out .= '<div class="snautofix" data-test-id="' . esc_attr( $test_name ) . '" data-test-status="' . esc_attr( $outstatus ) . '"></div>';
1417 $out .= '</div>';
1418 $out .= '</td><td><div class="sn-details"><a data-test-id="' . esc_attr( $test_name ) . '" data-test-status="' . esc_attr( $outstatus ) . '" href="#' . esc_attr( $test_name ) . '" class="action">' . $details_label . '</a></div>';
1419 $out .= '</td></tr>';
1420 }
1421 }
1422 $out .= '</tbody>';
1423 $out .= '<tfoot><tr>';
1424 $out .= '<th class="manage-column column-cb check-column"><label class="screen-reader-text" for="cb-select-all-2">' . __( 'Select All', 'security-ninja' ) . '</label><input id="cb-select-all-2" type="checkbox"></th>';
1425 $out .= '<th></th>';
1426 $out .= '<th class="column-primary">' . __( 'Security Test', 'security-ninja' ) . '</th><th>Actions</th>';
1427 $out .= '</tr></tfoot>';
1428 $out .= '</table>';
1429 $allowed_html = array_merge( wp_kses_allowed_html( 'post' ), array(
1430 'input' => array(
1431 'type' => array(),
1432 'name' => array(),
1433 'id' => array(),
1434 'value' => array(),
1435 'checked' => array(),
1436 'class' => array(),
1437 ),
1438 ) );
1439 $out = apply_filters( 'sn_tests_table', $out, $tests );
1440 echo wp_kses( $out, $allowed_html );
1441 ?>
1442 <p>
1443 <?php
1444 esc_html_e( 'Although these tests cover years of best practices in security, getting all test green does not guarantee your site will not get hacked. Likewise, having them all red does not mean you will get hacked.', 'security-ninja' );
1445 ?>
1446 </p>
1447 <p>
1448 <?php
1449 esc_html_e( "Please read each test's detailed information to see if it represents a real security issue for your site.", 'security-ninja' );
1450 ?>
1451 </p>
1452 </div>
1453 <?php
1454 }
1455
1456 /**
1457 * Returns all details about a test in JSON - used in AJAX
1458 *
1459 * @author Lars Koudal
1460 * @since v0.0.1
1461 * @version v1.0.0 Saturday, March 5th, 2022.
1462 * @access public static
1463 * @return void
1464 */
1465 public static function get_single_test_details() {
1466 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1467 check_ajax_referer( 'wf_sn_run_tests' );
1468 }
1469 if ( !current_user_can( 'manage_options' ) ) {
1470 wp_send_json_error( array(
1471 'message' => __( 'Failed.', 'security-ninja' ),
1472 ) );
1473 }
1474 if ( isset( $_POST['testid'] ) ) {
1475 $testid = sanitize_key( $_POST['testid'] );
1476 if ( $testid !== $_POST['testid'] ) {
1477 wp_send_json_error();
1478 }
1479 global $wpdb;
1480 $table_name = $wpdb->prefix . 'wf_sn_tests';
1481 $testdata = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wf_sn_tests WHERE testid = %s", $testid ) );
1482 if ( $testdata ) {
1483 wp_send_json_success( $testdata );
1484 } else {
1485 wp_send_json_error();
1486 }
1487 } else {
1488 wp_send_json_error();
1489 }
1490 die;
1491 }
1492
1493 /**
1494 * Returns long help text for all security tests (JSON) for lazy-loaded UI.
1495 *
1496 * @since 5.278
1497 * @return void
1498 */
1499 public static function get_test_descriptions() {
1500 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1501 check_ajax_referer( 'wf_sn_run_tests' );
1502 }
1503 if ( !current_user_can( 'manage_options' ) ) {
1504 wp_send_json_error( array(
1505 'message' => __( 'Failed.', 'security-ninja' ),
1506 ) );
1507 }
1508 $payload = Wf_Sn_Test_Descriptions::get_payload_for_tests_ui();
1509 wp_send_json_success( array(
1510 'tests' => $payload,
1511 ) );
1512 }
1513
1514 /**
1515 * Runs single test via AJAX call
1516 *
1517 * @author Lars Koudal
1518 * @since v0.0.1
1519 * @version v1.0.0 Thursday, January 14th, 2021.
1520 * @access public static
1521 * @return void
1522 */
1523 public static function run_single_test() {
1524 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1525 check_ajax_referer( 'wf_sn_run_tests' );
1526 }
1527 if ( !current_user_can( 'manage_options' ) ) {
1528 wp_send_json_error( array(
1529 'message' => __( 'Error. Please contact support', 'security-ninja' ),
1530 ) );
1531 }
1532 if ( isset( $_POST['stepid'] ) ) {
1533 $stepid = intval( $_POST['stepid'] );
1534 // Validate and sanitize the testarr input
1535 $testarr = array();
1536 if ( isset( $_POST['testarr'] ) && is_array( $_POST['testarr'] ) ) {
1537 // Only allow alphanumeric characters, underscores, and hyphens in test IDs
1538 $testarr = array_filter( $_POST['testarr'], function ( $test_id ) {
1539 return preg_match( '/^[a-zA-Z0-9_-]+$/', $test_id );
1540 } );
1541 }
1542 if ( !isset( $testarr[$stepid] ) ) {
1543 return false;
1544 }
1545 $response = false;
1546 $testid = sanitize_key( $testarr[$stepid] );
1547 if ( $testid ) {
1548 \WPSecurityNinja\Plugin\Utils::timerstart( $testid );
1549 $response = wf_sn_tests::$testid();
1550 }
1551 if ( $response ) {
1552 $json_response = array();
1553 if ( isset( $testarr[$stepid + 1] ) ) {
1554 $json_response['nexttest'] = $stepid + 1;
1555 } else {
1556 $json_response['nexttest'] = -1;
1557 }
1558 $security_tests = wf_sn_tests::return_security_tests();
1559 // allow overwriting with function response
1560 if ( isset( $response['msg_bad'] ) ) {
1561 $test['msg_bad'] = $response['msg_bad'];
1562 }
1563 if ( isset( $response['msg_ok'] ) ) {
1564 $test['msg_ok'] = $response['msg_ok'];
1565 }
1566 if ( isset( $response['msg_warning'] ) ) {
1567 $test['msg_warning'] = $response['msg_warning'];
1568 }
1569 if ( !isset( $response['msg'] ) ) {
1570 $response['msg'] = '';
1571 }
1572 $json_response['msg'] = $response['msg'];
1573 // Get the previous status from the database table
1574 $previous_status = null;
1575 global $wpdb;
1576 $table_name = $wpdb->prefix . 'wf_sn_tests';
1577 $previous_test = $wpdb->get_row( $wpdb->prepare( "SELECT status FROM {$table_name} WHERE testid = %s", $testid ) );
1578 if ( $previous_test ) {
1579 $previous_status = $previous_test->status;
1580 }
1581 // Check if status changed - ensure both are integers for comparison
1582 $previous_status_int = ( $previous_status !== null ? intval( $previous_status ) : null );
1583 $new_status_int = intval( $response['status'] );
1584 $status_changed = $previous_status_int !== $new_status_int;
1585 // Determine change direction for highlighting
1586 $change_direction = null;
1587 if ( $status_changed && $previous_status_int !== null ) {
1588 if ( $new_status_int === 10 && $previous_status_int < 10 ) {
1589 $change_direction = 'improved';
1590 // Went from fail/warning to pass
1591 } elseif ( 10 === $previous_status_int && $new_status_int < 10 ) {
1592 $change_direction = 'declined';
1593 // Went from pass to fail/warning
1594 } else {
1595 $change_direction = 'changed';
1596 // Other status changes
1597 }
1598 }
1599 $json_response['status_changed'] = $status_changed;
1600 $json_response['previous_status'] = $previous_status_int;
1601 $json_response['new_status'] = $new_status_int;
1602 $json_response['change_direction'] = $change_direction;
1603 // Return the correct status icon HTML for the frontend
1604 if ( 10 === $response['status'] ) {
1605 $json_response['status_icon'] = '<span class="teststatus pass">' . __( '', 'security-ninja' ) . '</span>';
1606 } elseif ( 0 === $response['status'] ) {
1607 $json_response['status_icon'] = '<span class="teststatus fail">' . __( '', 'security-ninja' ) . '</span>';
1608 } else {
1609 $json_response['status_icon'] = '<span class="teststatus warning">' . __( '&#9888;', 'security-ninja' ) . '</span>';
1610 }
1611 // Keep the label for backward compatibility
1612 if ( 10 === $response['status'] ) {
1613 $json_response['label'] = '<span class="wf-sn-label sn-success">' . __( 'OK', 'security-ninja' ) . '</span>';
1614 } elseif ( 0 === $response['status'] ) {
1615 $json_response['label'] = '<span class="wf-sn-label sn-error">' . __( 'Fail', 'security-ninja' ) . '</span>';
1616 } else {
1617 $json_response['label'] = '<span class="wf-sn-label sn-warning">' . __( 'Warning', 'security-ninja' ) . '</span>';
1618 }
1619 $json_response['status'] = $response['status'];
1620 $testscorearr = array(
1621 'testid' => $testid,
1622 'timestamp' => current_time( 'mysql' ),
1623 'title' => $security_tests[$testid]['title'],
1624 'status' => $response['status'],
1625 'score' => $security_tests[$testid]['score'],
1626 'msg' => $json_response['msg'],
1627 );
1628 // A way to add details
1629 if ( isset( $response['details'] ) ) {
1630 $testscorearr['details'] = $response['details'];
1631 $json_response['details'] = $response['details'];
1632 }
1633 $endtime = \WPSecurityNinja\Plugin\Utils::timerstop( $testid );
1634 if ( $endtime ) {
1635 $testscorearr['runtime'] = $endtime;
1636 }
1637 self::update_test_score( $testscorearr );
1638 $scores = self::return_test_scores();
1639 if ( $scores ) {
1640 $json_response['scores'] = $scores;
1641 }
1642 wp_send_json_success( $json_response );
1643 } else {
1644 wp_send_json_error( $testid );
1645 }
1646 }
1647 wp_send_json_error( '$stepid not set' );
1648 die;
1649 }
1650
1651 /**
1652 * Reset Secret Access URL via AJAX
1653 *
1654 * @author Lars Koudal
1655 * @since v0.0.1
1656 * @version v1.0.0 Monday, January 13th, 2025.
1657 * @access public static
1658 * @return void
1659 */
1660 public static function reset_secret_url() {
1661 check_ajax_referer( 'wf-sn-reset-secret-url' );
1662 if ( !current_user_can( 'manage_options' ) ) {
1663 wp_send_json_error( array(
1664 'message' => esc_html__( 'You do not have sufficient permissions to perform this action.', 'security-ninja' ),
1665 ) );
1666 }
1667 // Check if firewall module is available
1668 if ( !class_exists( __NAMESPACE__ . '\\Wf_sn_cf' ) ) {
1669 wp_send_json_error( array(
1670 'message' => esc_html__( 'Firewall module not available.', 'security-ninja' ),
1671 ) );
1672 }
1673 // Get firewall options
1674 $firewall_options = \WPSecurityNinja\Plugin\Wf_sn_cf::get_options();
1675 // Generate new secret access URL
1676 $firewall_options['unblock_url'] = md5( time() . wp_rand() );
1677 // Update the options
1678 update_option( 'wf_sn_cf', $firewall_options, false );
1679 // Log the event
1680 if ( class_exists( '\\WPSecurityNinja\\Plugin\\Wf_Sn_El_Modules' ) ) {
1681 \WPSecurityNinja\Plugin\Wf_Sn_El_Modules::log_event( 'security_ninja', 'secret_access_url_reset', 'Secret access URL was reset by user ID: ' . get_current_user_id() );
1682 }
1683 // Get the new URL for response
1684 $new_url = \WPSecurityNinja\Plugin\Wf_sn_cf::get_unblock_url();
1685 // Set a transient to show notice on page reload
1686 set_transient( 'sn_secret_url_reset_success', $new_url, 60 );
1687 wp_send_json_success( array(
1688 'message' => esc_html__( 'Secret access URL has been reset successfully.', 'security-ninja' ),
1689 ) );
1690 }
1691
1692 /**
1693 * Runs the tests
1694 *
1695 * @author Lars Koudal
1696 * @since v0.0.1
1697 * @version v1.0.0 Thursday, January 14th, 2021.
1698 * @access public static
1699 * @param bool $return_response Default: false.
1700 * @return void
1701 */
1702 public static function run_tests( $return_response = false ) {
1703 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1704 // Attempt to verify the first nonce
1705 $nonce_verified = wp_verify_nonce( sanitize_text_field( $_REQUEST['_wpnonce'] ), 'wf_sn_run_tests' );
1706 // If the first nonce verification fails, try the second nonce
1707 if ( !$nonce_verified ) {
1708 $nonce_verified = wp_verify_nonce( sanitize_text_field( $_REQUEST['_wpnonce'] ), 'secnin_scheduled_scanner' );
1709 }
1710 // If both nonce verifications fail, terminate the AJAX call
1711 if ( !$nonce_verified ) {
1712 wp_die( 'Nonce verification failed.', 'Nonce Verification', array(
1713 'response' => 403,
1714 ) );
1715 }
1716 }
1717 if ( !current_user_can( 'manage_options' ) ) {
1718 wp_send_json_error( array(
1719 'success' => false,
1720 'message' => esc_html__( 'You do not have permission to do this.', 'security-ninja' ),
1721 ) );
1722 }
1723 $step = ( isset( $_POST['step'] ) ? absint( $_POST['step'] ) : 1 );
1724 if ( 1 === $step ) {
1725 \WPSecurityNinja\Plugin\Utils::timerstart( 'wf_sn_run_tests' );
1726 }
1727 if ( !$step ) {
1728 $step = 0;
1729 }
1730 ++$step;
1731 $json_response = array();
1732 if ( $step ) {
1733 $json_response['step'] = $step;
1734 }
1735 $security_tests = \WPSecurityNinja\Plugin\Wf_Sn_Tests::return_security_tests();
1736 $totaltests = 0;
1737 if ( $security_tests ) {
1738 $totaltests = count( $security_tests );
1739 $json_response['totaltests'] = $totaltests;
1740 }
1741 $set_time_limit = set_time_limit( 200 );
1742 $loop_count = 1;
1743 $start_time = microtime( true );
1744 $test_description['last_run'] = time();
1745 if ( is_array( $security_tests ) ) {
1746 foreach ( $security_tests as $test_name => $test ) {
1747 if ( '_' === $test_name[0] || in_array( $test_name, self::$skip_tests, true ) || 'ad_' === substr( $test_name, 0, 3 ) ) {
1748 continue;
1749 }
1750 // If this is the one to be tested ...
1751 if ( $step === $loop_count ) {
1752 $response = wf_sn_tests::$test_name();
1753 $json_response['last_test'] = $test['title'];
1754 if ( isset( $response['status'] ) ) {
1755 $json_response['last_status'] = $response['status'];
1756 }
1757 $json_response['last_score'] = $test['score'];
1758 // allow overwriting with function response
1759 if ( isset( $response['msg_bad'] ) ) {
1760 $test['msg_bad'] = $response['msg_bad'];
1761 }
1762 if ( isset( $response['msg_ok'] ) ) {
1763 $test['msg_ok'] = $response['msg_ok'];
1764 }
1765 if ( isset( $response['msg_warning'] ) ) {
1766 $test['msg_warning'] = $response['msg_warning'];
1767 }
1768 if ( !isset( $response['msg'] ) ) {
1769 $response['msg'] = '';
1770 }
1771 if ( 10 === intval( $response['status'] ) ) {
1772 $json_response['last_msg'] = sprintf( (string) ($test['msg_ok'] ?? '%s'), $response['msg'] ?? '' );
1773 } elseif ( 0 === intval( $response['status'] ) ) {
1774 $json_response['last_msg'] = sprintf( (string) ($test['msg_bad'] ?? '%s'), $response['msg'] ?? '' );
1775 } else {
1776 $json_response['last_msg'] = sprintf( (string) ($test['msg_warning'] ?? '%s'), $response['msg'] ?? '' );
1777 }
1778 // Updates the results
1779 $resultssofar['test'][$test_name] = array(
1780 'title' => $test['title'],
1781 'status' => $response['status'],
1782 'score' => $test['score'],
1783 'msg' => $json_response['last_msg'],
1784 );
1785 // A way to add details
1786 if ( isset( $response['details'] ) ) {
1787 $resultssofar['test'][$test_name]['details'] = $response['details'];
1788 }
1789 // No more tests - let us stop
1790 if ( $step >= $totaltests ) {
1791 $json_response['step'] = 'done';
1792 $resultssofar['last_run'] = time();
1793 $stoptime = \WPSecurityNinja\Plugin\Utils::timerstop( 'wf_sn_run_tests' );
1794 if ( $stoptime ) {
1795 $resultssofar['run_time'] = $stoptime;
1796 }
1797 do_action( 'security_ninja_done_testing', $test_description, $resultssofar['run_time'] );
1798 }
1799 update_option( 'wf_sn_results', $resultssofar, false );
1800 wp_send_json_success( $json_response );
1801 }
1802 ++$loop_count;
1803 }
1804 }
1805 if ( $return_response ) {
1806 $resultssofar = get_option( 'wf_sn_results' );
1807 return $resultssofar;
1808 } else {
1809 wp_send_json_success( $json_response );
1810 }
1811 }
1812
1813 /**
1814 * Runs all tests with multisite compatibility
1815 *
1816 * @author Lars Koudal
1817 * @since v0.0.1
1818 * @version v1.0.0 Thursday, January 14th, 2021.
1819 * @version v1.0.1 Tuesday, June 11th, 2024.
1820 * @access public static
1821 * @param boolean $return_data Default: false
1822 * @return void
1823 */
1824 public static function run_all_tests( $return_data = false ) {
1825 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1826 // Attempt to verify the first nonce.
1827 $nonce_verified = wp_verify_nonce( sanitize_text_field( $_REQUEST['nonce'] ), 'wf_sn_run_tests' );
1828 // If the first nonce verification fails, try the second nonce
1829 if ( !$nonce_verified ) {
1830 $nonce = wp_unslash( $_REQUEST['nonce'] );
1831 $nonce = sanitize_text_field( $nonce );
1832 $nonce_verified = wp_verify_nonce( $nonce, 'secnin_scheduled_scanner' );
1833 }
1834 // If both nonce verifications fail, terminate the AJAX call.
1835 if ( !$nonce_verified ) {
1836 wp_die( 'Nonce verification failed.', 'Nonce Verification', array(
1837 'response' => 403,
1838 ) );
1839 }
1840 }
1841 \WPSecurityNinja\Plugin\Utils::timerstart( 'wf_sn_run_all_tests' );
1842 $security_tests = wf_sn_tests::return_security_tests();
1843 $resultssofar = array();
1844 $json_response = array();
1845 $set_time_limit = set_time_limit( 200 );
1846 $loop_count = 1;
1847 $resultssofar['last_run'] = time();
1848 // Add multisite context information
1849 if ( is_multisite() ) {
1850 $resultssofar['multisite'] = array(
1851 'is_multisite' => true,
1852 'current_site_id' => get_current_blog_id(),
1853 'is_main_site' => is_main_site(),
1854 'network_id' => get_current_network_id(),
1855 );
1856 } else {
1857 $resultssofar['multisite'] = array(
1858 'is_multisite' => false,
1859 );
1860 }
1861 if ( is_array( $security_tests ) ) {
1862 foreach ( $security_tests as $test_name => $test ) {
1863 if ( '_' === $test_name[0] || in_array( $test_name, self::$skip_tests, true ) || 'ad_' === substr( $test_name, 0, 3 ) ) {
1864 continue;
1865 }
1866 // Skip certain tests in multisite subsites if they're network-level concerns
1867 if ( is_multisite() && !is_main_site() && self::should_skip_test_in_subsite( $test_name ) ) {
1868 $resultssofar['test'][$test_name] = array(
1869 'title' => $test['title'],
1870 'status' => 5,
1871 'score' => $test['score'],
1872 'msg' => __( 'This test is typically handled at the network level in multisite installations.', 'security-ninja' ),
1873 );
1874 ++$loop_count;
1875 continue;
1876 }
1877 $response = wf_sn_tests::$test_name();
1878 $json_response = array();
1879 $json_response['last_test'] = $test['title'];
1880 $json_response['last_status'] = $response['status'];
1881 $json_response['last_score'] = $test['score'];
1882 if ( !isset( $response['msg'] ) ) {
1883 $response['msg'] = '';
1884 }
1885 // Setting appropriate message.
1886 if ( 10 === intval( $response['status'] ) ) {
1887 $json_response['last_msg'] = $response['msg'];
1888 } elseif ( 0 === intval( $response['status'] ) ) {
1889 $json_response['last_msg'] = $response['msg'];
1890 } else {
1891 $json_response['last_msg'] = $response['msg'];
1892 }
1893 // Updates the results
1894 $resultssofar['test'][$test_name] = array(
1895 'title' => $test['title'],
1896 'status' => $response['status'],
1897 'score' => $test['score'],
1898 'msg' => $json_response['last_msg'],
1899 'details' => ( isset( $response['details'] ) ? $response['details'] : '' ),
1900 );
1901 // Persist to wf_sn_tests table so get_test_results() and AI Advisor have latest findings.
1902 $testresult = array(
1903 'testid' => $test_name,
1904 'timestamp' => current_time( 'mysql' ),
1905 'title' => $test['title'],
1906 'status' => (int) $response['status'],
1907 'score' => (int) $test['score'],
1908 'runtime' => '',
1909 'msg' => $json_response['last_msg'],
1910 'details' => ( isset( $response['details'] ) ? $response['details'] : '' ),
1911 );
1912 self::update_test_score( $testresult );
1913 ++$loop_count;
1914 }
1915 // No more tests - let us stop
1916 $json_response['step'] = 'done';
1917 $resultssofar['last_run'] = time();
1918 $stoptime = \WPSecurityNinja\Plugin\Utils::timerstop( 'wf_sn_run_all_tests' );
1919 if ( $stoptime ) {
1920 $resultssofar['run_time'] = $stoptime;
1921 }
1922 update_option( 'wf_sn_results', $resultssofar, false );
1923 }
1924 // her stopper det sjove?
1925 do_action( 'security_ninja_done_testing', __( 'Security Tests - Completed Scanning', 'security-ninja' ), $resultssofar['run_time'] );
1926 if ( $return_data ) {
1927 $resultssofar = get_option( 'wf_sn_results' );
1928 return $resultssofar;
1929 } else {
1930 wp_send_json_success( $json_response );
1931 }
1932 }
1933
1934 /**
1935 * Determines if a test should be skipped in multisite subsites
1936 *
1937 * @author Lars Koudal
1938 * @since v1.0.1
1939 * @version v1.0.0 Tuesday, June 11th, 2024.
1940 * @access private static
1941 * @param string $test_name The name of the test
1942 * @return boolean True if the test should be skipped
1943 */
1944 private static function should_skip_test_in_subsite( $test_name ) {
1945 // Tests that are typically network-level concerns in multisite
1946 $network_level_tests = array(
1947 'core_updates_check',
1948 'config_chmod',
1949 'config_location',
1950 'db_password_check',
1951 'salt_keys_check',
1952 'salt_keys_age_check',
1953 'register_globals_check',
1954 'safe_mode_check',
1955 'allow_url_include_check',
1956 'expose_php_check',
1957 'display_errors_check',
1958 'php_headers',
1959 'strict_transport_security',
1960 'referrer_policy',
1961 'feature_policy',
1962 'content_security_policy',
1963 'x_frame_options',
1964 'x_content_type_options'
1965 );
1966 return in_array( $test_name, $network_level_tests, true );
1967 }
1968
1969 /**
1970 * Resets pointers on activation and saves some information.
1971 *
1972 * This method is responsible for resetting pointers and saving specific information during the plugin's activation process.
1973 * It checks if the plugin is being activated for the first time and sets the initial version and install time if so.
1974 * Additionally, it may create a database table for storing security test results and performs other activation routines.
1975 *
1976 * @author Lars Koudal
1977 * @since v0.0.1
1978 * @version v1.0.0 Tuesday, December 7th, 2021.
1979 * @access public static
1980 * @param bool $network_wide Whether the plugin is being activated network-wide.
1981 * @return void
1982 */
1983 public static function activate( $network_wide = false ) {
1984 $missing_options_marker = '__secnin_missing_options__';
1985 $is_first_install = $missing_options_marker === get_option( 'wf_sn_options', $missing_options_marker );
1986 $options = self::get_options();
1987 if ( $is_first_install ) {
1988 $options['first_version'] = \WPSecurityNinja\Plugin\Utils::get_plugin_version();
1989 $options['first_install'] = time();
1990 update_option( 'wf_sn_options', $options, false );
1991 $action = ( isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '' );
1992 $checked_plugins = ( isset( $_POST['checked'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['checked'] ) ) : array() );
1993 $is_bulk_activate = 'activate-selected' === $action || count( $checked_plugins ) > 1;
1994 if ( !$network_wide && !$is_bulk_activate ) {
1995 $user_id = get_current_user_id();
1996 if ( $user_id ) {
1997 update_option( 'secnin_activation_redirect', $user_id, false );
1998 }
1999 }
2000 }
2001 global $wpdb;
2002 include_once ABSPATH . 'wp-admin/includes/upgrade.php';
2003 $charset = $wpdb->get_charset_collate();
2004 if ( $network_wide && is_multisite() ) {
2005 // Network activation - create tables for all sites
2006 $sites = get_sites( array(
2007 'fields' => 'ids',
2008 ) );
2009 foreach ( $sites as $site_id ) {
2010 switch_to_blog( $site_id );
2011 \WPSecurityNinja\Plugin\Utils::create_tables_for_site( $charset );
2012 // Ensure Events Logger is enabled by default on first activation
2013 if ( class_exists( __NAMESPACE__ . '\\Wf_Sn_El' ) ) {
2014 \WPSecurityNinja\Plugin\Wf_Sn_El::default_settings( false );
2015 }
2016 update_option( 'wf_sn_db_version', self::WF_SN_DB_VERSION );
2017 restore_current_blog();
2018 }
2019 } elseif ( !$network_wide ) {
2020 // Single site activation (only when not network-wide)
2021 \WPSecurityNinja\Plugin\Utils::create_tables_for_site( $charset );
2022 // Ensure Events Logger is enabled by default on first activation
2023 if ( class_exists( __NAMESPACE__ . '\\Wf_Sn_El' ) ) {
2024 \WPSecurityNinja\Plugin\Wf_Sn_El::default_settings( false );
2025 }
2026 update_option( 'wf_sn_db_version', self::WF_SN_DB_VERSION );
2027 }
2028 }
2029
2030 /**
2031 * Performs cleanup operations when the plugin is deactivated.
2032 *
2033 * Plugin and module deactivation callbacks remove their own data when the
2034 * central setting is enabled. This callback removes core plugin data last.
2035 *
2036 * @author Lars Koudal
2037 * @since v0.0.1
2038 * @version v1.0.0 Saturday, March 5th, 2022.
2039 * @access public static
2040 * @return void
2041 */
2042 public static function deactivate() {
2043 wp_clear_scheduled_hook( 'wf_sn_ai_advisor_cron' );
2044 wp_clear_scheduled_hook( 'wf_sn_refresh_security_snapshot_daily' );
2045 if ( function_exists( 'as_unschedule_all_actions' ) ) {
2046 as_unschedule_all_actions( 'wf_sn_ai_advisor_run_scheduled_report', array(), 'security-ninja' );
2047 }
2048 if ( !\WPSecurityNinja\Plugin\Utils::should_remove_settings_on_deactivate() ) {
2049 return;
2050 }
2051 \WPSecurityNinja\Plugin\Utils::remove_plugin_data( 'deactivate' );
2052 }
2053
2054 /**
2055 * Performs cleanup operations when the plugin is uninstalled.
2056 *
2057 * This method drops the security tests table, deletes various options and usermeta, and drops additional tables and options if the plugin is premium.
2058 *
2059 * @author Lars Koudal
2060 * @since v0.0.1
2061 * @version v1.0.0 Saturday, March 5th, 2022.
2062 * @access public static
2063 * @return void
2064 */
2065 public static function uninstall() {
2066 \WPSecurityNinja\Plugin\Utils::remove_plugin_data( 'uninstall' );
2067 }
2068
2069 /**
2070 * Updates test score in database - multisite compatible
2071 *
2072 * @author Lars Koudal
2073 * @since v0.0.1
2074 * @version v1.0.0 Thursday, January 14th, 2021.
2075 * @version v1.0.1 Tuesday, June 11th, 2024.
2076 * @access public static
2077 * @param array $testresult Test result data
2078 * @return boolean Success status
2079 */
2080 public static function update_test_score( $testresult ) {
2081 if ( !$testresult ) {
2082 return false;
2083 }
2084 global $wpdb;
2085 // Ensure we're using the correct database prefix for the current site
2086 $table_name = $wpdb->prefix . 'wf_sn_tests';
2087 if ( !isset( $testresult['details'] ) ) {
2088 $testresult['details'] = '';
2089 }
2090 // Ensure the table exists before attempting to insert/update
2091 if ( !self::table_exists( $table_name ) ) {
2092 // Create the table if it doesn't exist
2093 include_once ABSPATH . 'wp-admin/includes/upgrade.php';
2094 $charset = $wpdb->get_charset_collate();
2095 \WPSecurityNinja\Plugin\Utils::create_tables_for_site( $charset );
2096 }
2097 $result = $wpdb->replace( $table_name, $testresult, array(
2098 '%s',
2099 '%s',
2100 '%s',
2101 '%d',
2102 '%d',
2103 '%s',
2104 '%s'
2105 ) );
2106 return false !== $result;
2107 }
2108
2109 /**
2110 * Checks if a database table exists
2111 *
2112 * @author Lars Koudal
2113 * @since v1.0.1
2114 * @version v1.0.0 Tuesday, June 11th, 2024.
2115 * @access private static
2116 * @param string $table_name The table name to check
2117 * @return boolean True if the table exists
2118 */
2119 private static function table_exists( $table_name ) {
2120 global $wpdb;
2121 $result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) );
2122 return $result === $table_name;
2123 }
2124
2125 }
2126
2127 }
2128 register_activation_hook( __FILE__, array(__NAMESPACE__ . '\\WF_SN', 'activate') );
2129 register_deactivation_hook( __FILE__, array(__NAMESPACE__ . '\\WF_SN', 'deactivate') );
2130 register_uninstall_hook( __FILE__, array(__NAMESPACE__ . '\\WF_SN', 'uninstall') );
2131 // Load translations on init hook with priority 1 (WordPress 6.7+ requirement)
2132 // This ensures translations are loaded early in the init cycle, before any code uses them
2133 add_action( 'init', array(__NAMESPACE__ . '\\WF_SN', 'load_textdomain'), 1 );
2134 add_action( 'init', array(__NAMESPACE__ . '\\WF_SN', 'init') );
2135 // Multisite hooks
2136 if ( is_multisite() ) {
2137 add_action( 'wpmu_new_blog', array(__NAMESPACE__ . '\\WF_SN', 'handle_new_site') );
2138 add_action( 'wp_insert_site', array(__NAMESPACE__ . '\\WF_SN', 'handle_new_site') );
2139 }