| 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 & 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">' . __( '⚠', '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 |
} |