--- a/wp-simple-firewall/icwp-wpsf.php
+++ b/wp-simple-firewall/icwp-wpsf.php
@@ -3,7 +3,7 @@
* Plugin Name: Shield Security
* Plugin URI: https://clk.shldscrty.com/2f
* Description: Powerful, Easy-To-Use #1 Rated WordPress Security System
- * Version: 21.0.8
+ * Version: 21.0.10
* Text Domain: wp-simple-firewall
* Domain Path: /languages
* Author: Shield Security
@@ -13,7 +13,7 @@
*/
/**
- * Copyright (c) 2025 Shield Security <support@getshieldsecurity.com>
+ * Copyright (c) 2026 Shield Security <support@getshieldsecurity.com>
* All rights reserved.
* "Shield" (formerly WordPress Simple Firewall) is distributed under the GNU
* General Public License, Version 2, June 1991. Copyright (C) 1989, 1991 Free
--- a/wp-simple-firewall/plugin_autoload.php
+++ b/wp-simple-firewall/plugin_autoload.php
@@ -1,6 +1,8 @@
-<?php declare( strict_types=1 );
-
-require_once( __DIR__.'/src/lib/vendor/autoload.php' );
-
-/** We initialise our Carbon early. */
+<?php declare( strict_types=1 );
+
+if ( !defined( 'ABSPATH' ) ) { exit(); }
+
+require_once( __DIR__.'/src/lib/vendor/autoload.php' );
+
+/** We initialise our Carbon early. */
@class_exists( 'CarbonCarbon' );
No newline at end of file
--- a/wp-simple-firewall/plugin_compatibility.php
+++ b/wp-simple-firewall/plugin_compatibility.php
@@ -1,43 +1,45 @@
-<?php declare( strict_types=1 );
-
-new class() {
-
- private ?string $incompatible = null;
-
- private ?string $shield = null;
-
- public function __construct() {
- if ( function_exists( 'wp_get_active_and_valid_plugins' ) ) {
- foreach ( wp_get_active_and_valid_plugins() as $file ) {
- if ( str_ends_with( $file, '/wp-rss-aggregator.php' ) && function_exists( 'get_plugin_data' ) ) {
- $data = get_plugin_data( $file );
- if ( !empty( $data[ 'Version' ] ) && version_compare( (string)$data[ 'Version' ], '5.0', '<' ) ) {
- $this->incompatible = $data[ 'Name' ] ?? basename( $file );
- }
- }
- elseif ( str_ends_with( $file, '/icwp-wpsf.php' ) ) {
- $this->shield = $file;
- }
- }
-
- if ( !empty( $this->incompatible ) && !empty( $this->shield )
- && !file_exists( path_join( dirname( $this->shield ), 'ignore_incompatibilities' ) ) ) {
- add_action( 'admin_notices', fn() => $this->showIncompatibleNotice() );
- add_action( 'network_admin_notices', fn() => $this->showIncompatibleNotice() );
- throw new Exception( 'Incompatible plugin discovered.' );
- }
- }
- }
-
- private function showIncompatibleNotice() :void {
- echo sprintf(
- '<div class="error"><h4 style="margin-bottom: 7px;">%s</h4><p>%s</p></div>',
- sprintf( 'Shield Security - Potential Incompatible Plugin Detected: %s', esc_html( $this->incompatible ) ),
- implode( '<br/>', [
- 'Shield Security has detected that another plugin active on your site is potentially incompatible and may cause errors while running alongside Shield.',
- "To prevent crashing your site, Shield won't run and you may chose to deactivate the incompatible plugin.",
- sprintf( 'The incompatible plugin is: <strong>%s</strong>', esc_html( $this->incompatible ) )
- ] ),
- );
- }
+<?php declare( strict_types=1 );
+
+if ( !defined( 'ABSPATH' ) ) exit();
+
+new class() {
+
+ private ?string $incompatible = null;
+
+ private ?string $shield = null;
+
+ public function __construct() {
+ if ( function_exists( 'wp_get_active_and_valid_plugins' ) ) {
+ foreach ( wp_get_active_and_valid_plugins() as $file ) {
+ if ( str_ends_with( $file, '/wp-rss-aggregator.php' ) && function_exists( 'get_plugin_data' ) ) {
+ $data = get_plugin_data( $file );
+ if ( !empty( $data[ 'Version' ] ) && version_compare( (string)$data[ 'Version' ], '5.0', '<' ) ) {
+ $this->incompatible = $data[ 'Name' ] ?? basename( $file );
+ }
+ }
+ elseif ( str_ends_with( $file, '/icwp-wpsf.php' ) ) {
+ $this->shield = $file;
+ }
+ }
+
+ if ( !empty( $this->incompatible ) && !empty( $this->shield )
+ && !file_exists( path_join( dirname( $this->shield ), 'ignore_incompatibilities' ) ) ) {
+ add_action( 'admin_notices', fn() => $this->showIncompatibleNotice() );
+ add_action( 'network_admin_notices', fn() => $this->showIncompatibleNotice() );
+ throw new Exception( 'Incompatible plugin discovered.' );
+ }
+ }
+ }
+
+ private function showIncompatibleNotice() :void {
+ echo sprintf(
+ '<div class="error"><h4 style="margin-bottom: 7px;">%s</h4><p>%s</p></div>',
+ sprintf( 'Shield Security - Potential Incompatible Plugin Detected: %s', esc_html( $this->incompatible ) ),
+ implode( '<br/>', [
+ 'Shield Security has detected that another plugin active on your site is potentially incompatible and may cause errors while running alongside Shield.',
+ "To prevent crashing your site, Shield won't run and you may chose to deactivate the incompatible plugin.",
+ sprintf( 'The incompatible plugin is: <strong>%s</strong>', esc_html( $this->incompatible ) )
+ ] ),
+ );
+ }
};
No newline at end of file
--- a/wp-simple-firewall/plugin_init.php
+++ b/wp-simple-firewall/plugin_init.php
@@ -1,82 +1,79 @@
-<?php
-
-use FernleafSystemsWordpressPluginShieldController;
-use FernleafSystemsWordpressServicesServices;
-
-/** @var string $rootFile */
-global $oICWP_Wpsf;
-if ( isset( $oICWP_Wpsf ) ) {
- error_log( 'Attempting to load the Shield Plugin twice?' );
- return;
-}
-if ( empty( $rootFile ) ) {
- error_log( 'Attempt to directly access plugin init file.' );
- return;
-}
-
-class ICWP_WPSF_Shield_Security {
-
- /**
- * @var ICWP_WPSF_Shield_Security
- */
- private static $oInstance = null;
-
- /**
- * @var ControllerController
- */
- private $con;
-
- private function __construct( ControllerController $controller ) {
- $this->con = $controller;
- }
-
- /**
- * @throws Exception
- */
- public function start() {
- $this->con->boot();
- }
-
- public function getController() :ControllerController {
- return ControllerController::GetInstance();
- }
-
- /**
- * @throws Exception
- */
- public static function GetInstance( ?ControllerController $con = null ) :ICWP_WPSF_Shield_Security {
- if ( is_null( self::$oInstance ) ) {
- if ( !$con instanceof ControllerController ) {
- throw new Exception( 'Trying to create a Shield Plugin instance without a valid Controller' );
- }
- self::$oInstance = new self( $con );
- }
- return self::$oInstance;
- }
-}
-
-Services::GetInstance();
-
-try {
- $oICWP_Wpsf_Controller = ControllerController::GetInstance( $rootFile );
- $oICWP_Wpsf = ICWP_WPSF_Shield_Security::GetInstance( $oICWP_Wpsf_Controller );
- $oICWP_Wpsf->start();
-}
-catch ( ControllerExceptionsVersionMismatchException $e ) {
- add_action( 'admin_notices', function () use ( $e ) {
- echo sprintf( '<div class="notice error"><p>%s</p></div>',
- 'Shield Security: There appears to be a configuration issue - please reinstall the Shield Security plugin.' );
- } );
-}
-catch ( ControllerExceptionsPluginConfigInvalidException $e ) {
- add_action( 'admin_notices', function () use ( $e ) {
- echo sprintf( '<div class="notice error"><p>%s</p><p>%s</p></div>',
- 'Shield Security: Could not load the plugin modules configuration. Please refresh and if the problem persists, please reinstall the Shield plugin.',
- $e->getMessage()
- );
- } );
-}
-catch ( Exception $e ) {
- error_log( 'Perhaps due to a failed upgrade, the Shield plugin failed to load certain component(s) - you should remove the plugin and reinstall.' );
- error_log( $e->getMessage() );
+<?php
+
+use FernleafSystemsWordpressPluginShieldController;
+use FernleafSystemsWordpressServicesServices;
+
+if ( !defined( 'ABSPATH' ) ) { exit(); }
+
+/** @var string $rootFile */
+global $oICWP_Wpsf;
+if ( isset( $oICWP_Wpsf ) || empty( $rootFile ) ) {
+ return;
+}
+
+class ICWP_WPSF_Shield_Security {
+
+ /**
+ * @var ICWP_WPSF_Shield_Security
+ */
+ private static $oInstance = null;
+
+ /**
+ * @var ControllerController
+ */
+ private $con;
+
+ private function __construct( ControllerController $controller ) {
+ $this->con = $controller;
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function start() {
+ $this->con->boot();
+ }
+
+ public function getController() :ControllerController {
+ return ControllerController::GetInstance();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public static function GetInstance( ?ControllerController $con = null ) :ICWP_WPSF_Shield_Security {
+ if ( is_null( self::$oInstance ) ) {
+ if ( !$con instanceof ControllerController ) {
+ throw new Exception( 'Trying to create a Shield Plugin instance without a valid Controller' );
+ }
+ self::$oInstance = new self( $con );
+ }
+ return self::$oInstance;
+ }
+}
+
+Services::GetInstance();
+
+try {
+ $oICWP_Wpsf_Controller = ControllerController::GetInstance( $rootFile );
+ $oICWP_Wpsf = ICWP_WPSF_Shield_Security::GetInstance( $oICWP_Wpsf_Controller );
+ $oICWP_Wpsf->start();
+}
+catch ( ControllerExceptionsVersionMismatchException $e ) {
+ add_action( 'admin_notices', function () use ( $e ) {
+ echo sprintf( '<div class="notice error"><p>%s</p></div>',
+ 'Shield Security: There appears to be a configuration issue - please reinstall the Shield Security plugin.' );
+ } );
+}
+catch ( ControllerExceptionsPluginConfigInvalidException $e ) {
+ add_action( 'admin_notices', function () use ( $e ) {
+ echo sprintf( '<div class="notice error"><p>%s</p><p>%s</p></div>',
+ 'Shield Security: Could not load the plugin modules configuration. Please refresh and if the problem persists, please reinstall the Shield plugin.',
+ $e->getMessage()
+ );
+ } );
+}
+catch ( Exception $e ) {
+ error_log( 'Perhaps due to a failed upgrade, the Shield plugin failed to load certain component(s) - you should remove the plugin and reinstall.' );
+ error_log( $e->getMessage() );
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/functions/functions.php
+++ b/wp-simple-firewall/src/lib/functions/functions.php
@@ -1,39 +1,39 @@
-<?php declare( strict_types=1 );
-
-use FernleafSystemsWordpressPluginShieldFunctions;
-
-if ( function_exists( 'shield_security_get_plugin' ) ) {
- return;
-}
-
-function shield_security_get_plugin() :ICWP_WPSF_Shield_Security {
- return Functionsget_plugin();
-}
-
-function shield_get_visitor_scores( $IP = null ) :array {
- return Functionsget_visitor_scores( $IP );
-}
-
-function shield_get_visitor_score( $IP = null ) :int {
- return Functionsget_visitor_score( $IP );
-}
-
-/**
- * @param null $IP - defaults to current visitor
- * @throws Exception
- */
-function shield_test_ip_is_bot( $IP = null ) :bool {
- return Functionstest_ip_is_bot( $IP );
-}
-
-function shield_get_ip_state( string $ip = '' ) :string {
- return Functionsget_ip_state( $ip );
-}
-
-function shield_fire_event( string $event ) {
- Functionsfire_event( $event );
-}
-
-function shield_start_scans( array $scans ) {
- Functionsstart_scans( $scans );
+<?php declare( strict_types=1 );
+
+use FernleafSystemsWordpressPluginShieldFunctions;
+
+if ( function_exists( 'shield_security_get_plugin' ) ) {
+ return;
+}
+
+function shield_security_get_plugin() :ICWP_WPSF_Shield_Security {
+ return Functionsget_plugin();
+}
+
+function shield_get_visitor_scores( $IP = null ) :array {
+ return Functionsget_visitor_scores( $IP );
+}
+
+function shield_get_visitor_score( $IP = null ) :int {
+ return Functionsget_visitor_score( $IP );
+}
+
+/**
+ * @param null $IP - defaults to current visitor
+ * @throws Exception
+ */
+function shield_test_ip_is_bot( $IP = null ) :bool {
+ return Functionstest_ip_is_bot( $IP );
+}
+
+function shield_get_ip_state( string $ip = '' ) :string {
+ return Functionsget_ip_state( $ip );
+}
+
+function shield_fire_event( string $event ) {
+ Functionsfire_event( $event );
+}
+
+function shield_start_scans( array $scans ) {
+ Functionsstart_scans( $scans );
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionData.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionData.php
@@ -1,77 +1,77 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsWordpressPluginShieldActionRouterActionsAjaxRender;
-use FernleafSystemsWordpressPluginShieldActionRouterActionsBaseAction;
-use FernleafSystemsWordpressServicesServices;
-use FernleafSystemsWordpressServicesUtilitiesPasswordGenerator;
-use FernleafSystemsWordpressServicesUtilitiesURL;
-
-class ActionData {
-
- public const FIELD_ACTION = 'action';
- public const FIELD_AJAXURL = 'ajaxurl';
- public const FIELD_SHIELD = 'shield_action';
- public const FIELD_EXECUTE = 'ex';
- public const FIELD_NONCE = 'exnonce';
- public const FIELD_REST_NONCE = '_wpnonce';
- public const FIELD_REST_URL = '_rest_url';
- public const FIELD_WRAP_RESPONSE = 'apto_wrap_response';
-
- public static function Build( string $actionClass, bool $isAjax = true, array $aux = [], bool $uniq = false ) :array {
- $vo = new ActionDataVO();
- $vo->action = $actionClass;
- $vo->is_ajax = $isAjax;
- $vo->aux = $aux;
- $vo->unique = $uniq;
- return self::BuildVO( $vo );
- }
-
- public static function BuildVO( ActionDataVO $VO ) :array {
- $data = array_merge( [
- self::FIELD_ACTION => self::FIELD_SHIELD,
- self::FIELD_EXECUTE => $VO->action::SLUG,
- self::FIELD_NONCE => ActionNonce::Create( $VO->action ),
- ], $VO->aux );
-
- if ( $VO->unique ) {
- $data[ 'uniq' ] = PasswordGenerator::Gen( 4, true, true, false );
- }
-
- if ( count( $VO->excluded_fields ) > 0 ) {
- $data = array_diff_key( $data, array_flip( $VO->excluded_fields ) );
- }
-
- if ( $VO->is_ajax ) {
- $data[ self::FIELD_AJAXURL ] = Services::WpGeneral()->ajaxURL();
-
- $data[ self::FIELD_REST_NONCE ] = wp_create_nonce( 'wp_rest' );
- $data[ self::FIELD_REST_URL ] = URL::Build(
- get_rest_url( null, sprintf( 'shield/v1/action/%s', $VO->action::SLUG ) ),
- array_diff_key(
- $data,
- array_flip( [
- self::FIELD_ACTION,
- self::FIELD_EXECUTE,
- self::FIELD_AJAXURL,
- ] )
- )
- );
- }
-
- return $data;
- }
-
- /**
- * @param BaseAction|string $actionClass
- */
- public static function BuildAjaxRender( string $actionClass = '', array $aux = [] ) :array {
- $aux[ 'render_slug' ] = empty( $actionClass ) ? '' : $actionClass::SLUG;
- return self::Build( AjaxRender::class, true, $aux );
- }
-
- public static function BuildJson( string $actionClass, bool $isAjax = true, array $aux = [] ) :string {
- return wp_json_encode( (object)self::Build( $actionClass, $isAjax, $aux ) );
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsWordpressPluginShieldActionRouterActionsAjaxRender;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsBaseAction;
+use FernleafSystemsWordpressServicesServices;
+use FernleafSystemsWordpressServicesUtilitiesPasswordGenerator;
+use FernleafSystemsWordpressServicesUtilitiesURL;
+
+class ActionData {
+
+ public const FIELD_ACTION = 'action';
+ public const FIELD_AJAXURL = 'ajaxurl';
+ public const FIELD_SHIELD = 'shield_action';
+ public const FIELD_EXECUTE = 'ex';
+ public const FIELD_NONCE = 'exnonce';
+ public const FIELD_REST_NONCE = '_wpnonce';
+ public const FIELD_REST_URL = '_rest_url';
+ public const FIELD_WRAP_RESPONSE = 'apto_wrap_response';
+
+ public static function Build( string $actionClass, bool $isAjax = true, array $aux = [], bool $uniq = false ) :array {
+ $vo = new ActionDataVO();
+ $vo->action = $actionClass;
+ $vo->is_ajax = $isAjax;
+ $vo->aux = $aux;
+ $vo->unique = $uniq;
+ return self::BuildVO( $vo );
+ }
+
+ public static function BuildVO( ActionDataVO $VO ) :array {
+ $data = array_merge( [
+ self::FIELD_ACTION => self::FIELD_SHIELD,
+ self::FIELD_EXECUTE => $VO->action::SLUG,
+ self::FIELD_NONCE => ActionNonce::Create( $VO->action ),
+ ], $VO->aux );
+
+ if ( $VO->unique ) {
+ $data[ 'uniq' ] = PasswordGenerator::Gen( 4, true, true, false );
+ }
+
+ if ( count( $VO->excluded_fields ) > 0 ) {
+ $data = array_diff_key( $data, array_flip( $VO->excluded_fields ) );
+ }
+
+ if ( $VO->is_ajax ) {
+ $data[ self::FIELD_AJAXURL ] = Services::WpGeneral()->ajaxURL();
+
+ $data[ self::FIELD_REST_NONCE ] = wp_create_nonce( 'wp_rest' );
+ $data[ self::FIELD_REST_URL ] = URL::Build(
+ get_rest_url( null, sprintf( 'shield/v1/action/%s', $VO->action::SLUG ) ),
+ array_diff_key(
+ $data,
+ array_flip( [
+ self::FIELD_ACTION,
+ self::FIELD_EXECUTE,
+ self::FIELD_AJAXURL,
+ ] )
+ )
+ );
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param BaseAction|string $actionClass
+ */
+ public static function BuildAjaxRender( string $actionClass = '', array $aux = [] ) :array {
+ $aux[ 'render_slug' ] = empty( $actionClass ) ? '' : $actionClass::SLUG;
+ return self::Build( AjaxRender::class, true, $aux );
+ }
+
+ public static function BuildJson( string $actionClass, bool $isAjax = true, array $aux = [] ) :string {
+ return wp_json_encode( (object)self::Build( $actionClass, $isAjax, $aux ) );
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionDataVO.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionDataVO.php
@@ -1,62 +1,62 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsUtilitiesDataAdapterDynPropertiesClass;
-use FernleafSystemsWordpressPluginShieldActionRouterActionsBaseAction;
-use FernleafSystemsWordpressServicesServices;
-
-/**
- * @property string|BaseAction $action
- * @property array $aux
- * @property bool $is_ajax
- * @property bool $ip_in_nonce
- * @property bool $is_rest
- * @property bool $unique
- * @property array $excluded_fields
- */
-class ActionDataVO extends DynPropertiesClass {
-
- /**
- * @throws Exception
- */
- public function __get( string $key ) {
- $val = parent::__get( $key );
-
- switch ( $key ) {
-
- case 'action_class':
- if ( empty( $val ) || !@class_exists( $val ) ) {
- throw new Exception( '$action_class is empty!' );
- }
- break;
- case 'aux':
- case 'excluded_fields':
- if ( !is_array( $val ) ) {
- $val = [];
- }
- break;
- case 'is_ajax':
- case 'ip_in_nonce':
- if ( !is_bool( $val ) ) {
- $val = true;
- }
- break;
- case 'unique':
- if ( !is_bool( $val ) ) {
- $val = false;
- }
- break;
- case 'is_rest':
- if ( !is_bool( $val ) ) {
- $val = $this->is_ajax && Services::WpUsers()->isUserLoggedIn();
- }
- break;
-
- default:
- break;
- }
-
- return $val;
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsUtilitiesDataAdapterDynPropertiesClass;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsBaseAction;
+use FernleafSystemsWordpressServicesServices;
+
+/**
+ * @property string|BaseAction $action
+ * @property array $aux
+ * @property bool $is_ajax
+ * @property bool $ip_in_nonce
+ * @property bool $is_rest
+ * @property bool $unique
+ * @property array $excluded_fields
+ */
+class ActionDataVO extends DynPropertiesClass {
+
+ /**
+ * @throws Exception
+ */
+ public function __get( string $key ) {
+ $val = parent::__get( $key );
+
+ switch ( $key ) {
+
+ case 'action_class':
+ if ( empty( $val ) || !@class_exists( $val ) ) {
+ throw new Exception( '$action_class is empty!' );
+ }
+ break;
+ case 'aux':
+ case 'excluded_fields':
+ if ( !is_array( $val ) ) {
+ $val = [];
+ }
+ break;
+ case 'is_ajax':
+ case 'ip_in_nonce':
+ if ( !is_bool( $val ) ) {
+ $val = true;
+ }
+ break;
+ case 'unique':
+ if ( !is_bool( $val ) ) {
+ $val = false;
+ }
+ break;
+ case 'is_rest':
+ if ( !is_bool( $val ) ) {
+ $val = $this->is_ajax && Services::WpUsers()->isUserLoggedIn();
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return $val;
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionNonce.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionNonce.php
@@ -1,55 +1,55 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsWordpressServicesServices;
-
-class ActionNonce {
-
- /**
- * @param ActionsBaseAction|string $action
- */
- public static function Create( string $action ) :string {
- return self::CreateNonces( $action )[ 0 ];
- }
-
- /**
- * @param ActionsBaseAction|string $action
- */
- public static function CreateNonces( string $action ) :array {
- return array_map(
- function ( int $distance ) use ( $action ) {
- $action = UtilityActionsMap::ActionFromSlug( $action );
-
- $nonceCfg = array_merge( [
- 'ip' => true,
- 'ttl' => 12,
- ], $action::NonceCfg() );
-
- return substr( wp_hash( implode( '|', [
- sprintf( '%s-%s', ActionData::FIELD_SHIELD, $action::SLUG ),
- Services::WpUsers()->getCurrentWpUserId(),
- $nonceCfg[ 'ip' ] ? Services::Request()->ip() : '-',
- ceil( Services::Request()->ts()/( HOUR_IN_SECONDS*$nonceCfg[ 'ttl' ] ) ) - $distance,
- ] ), 'nonce' ), -12, 10 );
- },
- [ 0, 1 ]
- );
- }
-
- public static function Verify( string $action, string $nonce ) :bool {
- $valid = false;
- foreach ( self::CreateNonces( $action ) as $expected ) {
- if ( hash_equals( $expected, $nonce ) ) {
- $valid = true;
- break;
- }
- }
- return $valid;
- }
-
- public static function VerifyFromRequest() :bool {
- $req = Services::Request();
- return self::Verify( (string)$req->request( ActionData::FIELD_EXECUTE ), (string)$req->request( ActionData::FIELD_NONCE ) );
- }
-}
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsWordpressServicesServices;
+
+class ActionNonce {
+
+ /**
+ * @param ActionsBaseAction|string $action
+ */
+ public static function Create( string $action ) :string {
+ return self::CreateNonces( $action )[ 0 ];
+ }
+
+ /**
+ * @param ActionsBaseAction|string $action
+ */
+ public static function CreateNonces( string $action ) :array {
+ return array_map(
+ function ( int $distance ) use ( $action ) {
+ $action = UtilityActionsMap::ActionFromSlug( $action );
+
+ $nonceCfg = array_merge( [
+ 'ip' => true,
+ 'ttl' => 12,
+ ], $action::NonceCfg() );
+
+ return substr( wp_hash( implode( '|', [
+ sprintf( '%s-%s', ActionData::FIELD_SHIELD, $action::SLUG ),
+ Services::WpUsers()->getCurrentWpUserId(),
+ $nonceCfg[ 'ip' ] ? Services::Request()->ip() : '-',
+ ceil( Services::Request()->ts()/( HOUR_IN_SECONDS*$nonceCfg[ 'ttl' ] ) ) - $distance,
+ ] ), 'nonce' ), -12, 10 );
+ },
+ [ 0, 1 ]
+ );
+ }
+
+ public static function Verify( string $action, string $nonce ) :bool {
+ $valid = false;
+ foreach ( self::CreateNonces( $action ) as $expected ) {
+ if ( hash_equals( $expected, $nonce ) ) {
+ $valid = true;
+ break;
+ }
+ }
+ return $valid;
+ }
+
+ public static function VerifyFromRequest() :bool {
+ $req = Services::Request();
+ return self::Verify( (string)$req->request( ActionData::FIELD_EXECUTE ), (string)$req->request( ActionData::FIELD_NONCE ) );
+ }
+}
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionProcessor.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionProcessor.php
@@ -1,41 +1,47 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsWordpressPluginShieldActionRouterExceptions{
- ActionDoesNotExistException,
- ActionException,
- InvalidActionNonceException,
- IpBlockedException,
- SecurityAdminRequiredException,
- UserAuthRequiredException,
-};
-use FernleafSystemsWordpressPluginShieldActionRouterUtilityActionsMap;
-
-class ActionProcessor {
-
- /**
- * @throws ActionDoesNotExistException
- * @throws ActionException
- * @throws InvalidActionNonceException
- * @throws IpBlockedException
- * @throws SecurityAdminRequiredException
- * @throws UserAuthRequiredException
- */
- public function processAction( string $classOrSlug, array $data = [] ) :ActionResponse {
- $action = $this->getAction( $classOrSlug, $data );
- $action->process();
- return $action->response();
- }
-
- /**
- * @throws ActionDoesNotExistException
- */
- public function getAction( string $classOrSlug, array $data ) :ActionsBaseAction {
- $action = ActionsMap::ActionFromSlug( $classOrSlug );
- if ( empty( $action ) ) {
- throw new ActionDoesNotExistException( 'There was no action handler available for '.$classOrSlug );
- }
- return new $action( $data );
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsWordpressPluginShieldActionRouterExceptions{
+ ActionDoesNotExistException,
+ ActionException,
+ InvalidActionNonceException,
+ IpBlockedException,
+ SecurityAdminRequiredException,
+ UserAuthRequiredException,
+};
+use FernleafSystemsWordpressPluginShieldActionRouterUtilityActionsMap;
+
+class ActionProcessor {
+
+ /**
+ * @throws ActionDoesNotExistException
+ * @throws ActionException
+ * @throws InvalidActionNonceException
+ * @throws IpBlockedException
+ * @throws SecurityAdminRequiredException
+ * @throws UserAuthRequiredException
+ */
+ public function processAction( string $classOrSlug, array $data = [] ) :ActionResponse {
+ $action = $this->getAction( $classOrSlug, $data );
+ $action->process();
+ return $action->response();
+ }
+
+ /**
+ * SECURITY FIX: Strip action_overrides from user input
+ * Security controls should never be controllable by user input, even from "authenticated" sources.
+ * This prevents CSRF bypass attacks where attackers send action_overrides[is_nonce_verify_required]=false
+ * Integrations that legitimately need overrides (like MainWP) should set them programmatically
+ * AFTER action creation using setActionOverride() method.
+ * @throws ActionDoesNotExistException
+ */
+ public function getAction( string $classOrSlug, array $data ) :ActionsBaseAction {
+ $action = ActionsMap::ActionFromSlug( $classOrSlug );
+ if ( empty( $action ) ) {
+ throw new ActionDoesNotExistException( 'There was no action handler available for '.esc_html( $classOrSlug ) );
+ }
+ unset( $data[ 'action_overrides' ] );
+ return new $action( $data );
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionResponse.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionResponse.php
@@ -1,36 +1,36 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsWordpressPluginShieldUtilitiesResponse;
-
-/**
- * @property string $action_slug
- * @property array $action_data
- * @property array $action_response_data
- *
- * @property array $next_step
- *
- * AJAX Actions:
- * @property array $ajax_data
- */
-class ActionResponse extends Response {
-
- public function __get( string $key ) {
- $value = parent::__get( $key );
- switch ( $key ) {
-
- case 'action_data':
- case 'action_response_data':
- case 'ajax_data':
- case 'render_data':
- case 'next_step':
- $value = is_array( $value ) ? $value : [];
- break;
-
- default:
- break;
- }
- return $value;
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsWordpressPluginShieldUtilitiesResponse;
+
+/**
+ * @property string $action_slug
+ * @property array $action_data
+ * @property array $action_response_data
+ *
+ * @property array $next_step
+ *
+ * AJAX Actions:
+ * @property array $ajax_data
+ */
+class ActionResponse extends Response {
+
+ public function __get( string $key ) {
+ $value = parent::__get( $key );
+ switch ( $key ) {
+
+ case 'action_data':
+ case 'action_response_data':
+ case 'ajax_data':
+ case 'render_data':
+ case 'next_step':
+ $value = is_array( $value ) ? $value : [];
+ break;
+
+ default:
+ break;
+ }
+ return $value;
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/ActionRoutingController.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/ActionRoutingController.php
@@ -1,97 +1,97 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouter;
-
-use FernleafSystemsUtilitiesLogicExecOnce;
-use FernleafSystemsWordpressPluginShieldActionRouterActionsRenderPluginAdminPagesPageSecurityAdminRestricted;
-use FernleafSystemsWordpressPluginShieldModulesPluginControllerConsumer;
-use FernleafSystemsWordpressServicesServices;
-
-class ActionRoutingController {
-
- use ExecOnce;
- use PluginControllerConsumer;
-
- public const ACTION_AJAX = 1;
- public const ACTION_SHIELD = 2;
- public const ACTION_REST = 3;
-
- protected function run() {
- ( new CaptureRedirects() )->run();
- ( new CapturePluginAction() )->execute();
- ( new CaptureAjaxAction() )->execute();
- ( new CaptureRestApiAction() )->execute();
- }
-
- /**
- * @throws ExceptionsActionDoesNotExistException
- * @throws ExceptionsActionException
- * @throws ExceptionsActionTypeDoesNotExistException
- * @throws ExceptionsSecurityAdminRequiredException
- * @throws ExceptionsInvalidActionNonceException
- */
- public function action( string $classOrSlug, array $data = [], int $type = self::ACTION_SHIELD ) :ActionResponse {
-
- switch ( $type ) {
- case self::ACTION_AJAX:
- $adapter = new ResponseAdapterAjaxResponseAdapter();
- break;
- case self::ACTION_SHIELD:
- $adapter = new ResponseAdapterShieldActionResponseAdapter();
- break;
- case self::ACTION_REST:
- $adapter = new ResponseAdapterRestApiActionResponseAdapter();
- break;
- default:
- throw new ExceptionsActionTypeDoesNotExistException( $type );
- }
-
- try {
- $response = ( new ActionProcessor() )->processAction( $classOrSlug, $data );
- }
- catch ( ExceptionsSecurityAdminRequiredException $sare ) {
- if ( Services::WpGeneral()->isAjax() ) {
- throw $sare;
- }
- $response = $this->action( ActionsRenderPluginAdminPagesPageSecurityAdminRestricted::class, $data );
- }
- catch ( ExceptionsInvalidActionNonceException $iane ) {
- if ( Services::WpGeneral()->isAjax() ) {
- throw $iane;
- }
- wp_die( sprintf( 'Unexpected data. Please try again. Action Slug: "%s"; Data: "%s"', $classOrSlug, var_export( $data, true ) ) );
- }
-
- $adapter->adapt( $response );
- return $response;
- }
-
- /**
- * This is an alias for calling the Render action directly
- */
- public function render( string $classOrSlug, array $data = [] ) :string {
- try {
- $output = $this->action(
- ActionsRender::class,
- [
- 'render_action_slug' => $classOrSlug,
- 'render_action_data' => $data,
- ]
- )->action_response_data[ 'render_output' ];
- }
- catch ( ExceptionsSecurityAdminRequiredException $e ) {
-// error_log( 'render::SecurityAdminRequiredException: '.$classOrSlug );
- $output = self::con()->action_router->render( PageSecurityAdminRestricted::class );
- }
- catch ( ExceptionsUserAuthRequiredException $uare ) {
-// error_log( 'render::UserAuthRequiredException: '.$classOrSlug );
- $output = '';
- }
- catch ( ExceptionsActionException $e ) {
-// error_log( 'render::ActionException: '.$classOrSlug.' '.$e->getMessage() );
- $output = $e->getMessage();
- }
-
- return $output;
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouter;
+
+use FernleafSystemsUtilitiesLogicExecOnce;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsRenderPluginAdminPagesPageSecurityAdminRestricted;
+use FernleafSystemsWordpressPluginShieldModulesPluginControllerConsumer;
+use FernleafSystemsWordpressServicesServices;
+
+class ActionRoutingController {
+
+ use ExecOnce;
+ use PluginControllerConsumer;
+
+ public const ACTION_AJAX = 1;
+ public const ACTION_SHIELD = 2;
+ public const ACTION_REST = 3;
+
+ protected function run() {
+ ( new CaptureRedirects() )->run();
+ ( new CapturePluginAction() )->execute();
+ ( new CaptureAjaxAction() )->execute();
+ ( new CaptureRestApiAction() )->execute();
+ }
+
+ /**
+ * @throws ExceptionsActionDoesNotExistException
+ * @throws ExceptionsActionException
+ * @throws ExceptionsActionTypeDoesNotExistException
+ * @throws ExceptionsSecurityAdminRequiredException
+ * @throws ExceptionsInvalidActionNonceException
+ */
+ public function action( string $classOrSlug, array $data = [], int $type = self::ACTION_SHIELD ) :ActionResponse {
+
+ switch ( $type ) {
+ case self::ACTION_AJAX:
+ $adapter = new ResponseAdapterAjaxResponseAdapter();
+ break;
+ case self::ACTION_SHIELD:
+ $adapter = new ResponseAdapterShieldActionResponseAdapter();
+ break;
+ case self::ACTION_REST:
+ $adapter = new ResponseAdapterRestApiActionResponseAdapter();
+ break;
+ default:
+ throw new ExceptionsActionTypeDoesNotExistException( $type );
+ }
+
+ try {
+ $response = ( new ActionProcessor() )->processAction( $classOrSlug, $data );
+ }
+ catch ( ExceptionsSecurityAdminRequiredException $sare ) {
+ if ( Services::WpGeneral()->isAjax() ) {
+ throw $sare;
+ }
+ $response = $this->action( ActionsRenderPluginAdminPagesPageSecurityAdminRestricted::class, $data );
+ }
+ catch ( ExceptionsInvalidActionNonceException $iane ) {
+ if ( Services::WpGeneral()->isAjax() ) {
+ throw $iane;
+ }
+ wp_die( sprintf( 'Unexpected data. Please try again. Action Slug: "%s"; Data: "%s"', $classOrSlug, var_export( $data, true ) ) );
+ }
+
+ $adapter->adapt( $response );
+ return $response;
+ }
+
+ /**
+ * This is an alias for calling the Render action directly
+ */
+ public function render( string $classOrSlug, array $data = [] ) :string {
+ try {
+ $output = $this->action(
+ ActionsRender::class,
+ [
+ 'render_action_slug' => $classOrSlug,
+ 'render_action_data' => $data,
+ ]
+ )->action_response_data[ 'render_output' ];
+ }
+ catch ( ExceptionsSecurityAdminRequiredException $e ) {
+// error_log( 'render::SecurityAdminRequiredException: '.$classOrSlug );
+ $output = self::con()->action_router->render( PageSecurityAdminRestricted::class );
+ }
+ catch ( ExceptionsUserAuthRequiredException $uare ) {
+// error_log( 'render::UserAuthRequiredException: '.$classOrSlug );
+ $output = '';
+ }
+ catch ( ExceptionsActionException $e ) {
+// error_log( 'render::ActionException: '.$classOrSlug.' '.$e->getMessage() );
+ $output = $e->getMessage();
+ }
+
+ return $output;
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/ActivityLogTableAction.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/ActivityLogTableAction.php
@@ -1,55 +1,55 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
-
-use FernleafSystemsWordpressPluginShieldDBsReqLogsGetRequestMeta;
-use FernleafSystemsWordpressPluginShieldTablesDataTablesLoadDataActivityLogBuildActivityLogTableData;
-
-class ActivityLogTableAction extends BaseAction {
-
- public const SLUG = 'logtable_action';
-
- protected function exec() {
- try {
- $action = $this->action_data[ 'sub_action' ];
- switch ( $action ) {
- case 'retrieve_table_data':
- $response = $this->retrieveTableData();
- break;
- case 'get_request_meta':
- $response = $this->getRequestMeta();
- break;
- default:
- throw new Exception( 'Not a supported Activity Log table sub_action: '.$action );
- }
- }
- catch ( Exception $e ) {
- $response = [
- 'success' => false,
- 'page_reload' => true,
- 'message' => $e->getMessage(),
- ];
- }
-
- $this->response()->action_response_data = $response;
- }
-
- private function retrieveTableData() :array {
- $builder = new BuildActivityLogTableData();
- $builder->table_data = $this->action_data[ 'table_data' ] ?? [];
- return [
- 'success' => true,
- 'datatable_data' => $builder->build(),
- ];
- }
-
- /**
- * @throws Exception
- */
- private function getRequestMeta() :array {
- return [
- 'success' => true,
- 'html' => ( new GetRequestMeta() )->retrieve( $this->action_data[ 'rid' ] ?? '' )
- ];
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsWordpressPluginShieldDBsReqLogsGetRequestMeta;
+use FernleafSystemsWordpressPluginShieldTablesDataTablesLoadDataActivityLogBuildActivityLogTableData;
+
+class ActivityLogTableAction extends BaseAction {
+
+ public const SLUG = 'logtable_action';
+
+ protected function exec() {
+ try {
+ $action = $this->action_data[ 'sub_action' ];
+ switch ( $action ) {
+ case 'retrieve_table_data':
+ $response = $this->retrieveTableData();
+ break;
+ case 'get_request_meta':
+ $response = $this->getRequestMeta();
+ break;
+ default:
+ throw new Exception( 'Not a supported Activity Log table sub_action: '.$action );
+ }
+ }
+ catch ( Exception $e ) {
+ $response = [
+ 'success' => false,
+ 'page_reload' => true,
+ 'message' => $e->getMessage(),
+ ];
+ }
+
+ $this->response()->action_response_data = $response;
+ }
+
+ private function retrieveTableData() :array {
+ $builder = new BuildActivityLogTableData();
+ $builder->table_data = $this->action_data[ 'table_data' ] ?? [];
+ return [
+ 'success' => true,
+ 'datatable_data' => $builder->build(),
+ ];
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function getRequestMeta() :array {
+ return [
+ 'success' => true,
+ 'html' => ( new GetRequestMeta() )->retrieve( $this->action_data[ 'rid' ] ?? '' )
+ ];
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/AjaxRender.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/AjaxRender.php
@@ -1,48 +1,48 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
-
-use FernleafSystemsWordpressPluginShieldActionRouterActionData;
-use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsAnyUserAuthRequired;
-use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsSecurityAdminNotRequired;
-
-class AjaxRender extends BaseAction {
-
- use AnyUserAuthRequired;
- use SecurityAdminNotRequired;
-
- public const SLUG = 'ajax_render';
-
- protected function exec() {
- $response = self::con()->action_router->action(
- $this->action_data[ 'render_slug' ],
- $this->getParamsMinusAjax()
- );
- foreach ( [ 'success', 'message', 'error' ] as $item ) {
- if ( isset( $response->action_response_data[ $item ] ) ) {
- $response->{$item} = $response->action_response_data[ $item ];
- }
- }
-
- $this->setResponse( $response );
- }
-
- protected function getParamsMinusAjax() :array {
- return array_diff_key(
- $this->action_data,
- array_flip( [
- ActionData::FIELD_ACTION,
- ActionData::FIELD_EXECUTE,
- ActionData::FIELD_NONCE,
- ActionData::FIELD_WRAP_RESPONSE,
- 'render_slug'
- ] )
- );
- }
-
- protected function getRequiredDataKeys() :array {
- return [
- 'render_slug'
- ];
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsWordpressPluginShieldActionRouterActionData;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsAnyUserAuthRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsSecurityAdminNotRequired;
+
+class AjaxRender extends BaseAction {
+
+ use AnyUserAuthRequired;
+ use SecurityAdminNotRequired;
+
+ public const SLUG = 'ajax_render';
+
+ protected function exec() {
+ $response = self::con()->action_router->action(
+ $this->action_data[ 'render_slug' ],
+ $this->getParamsMinusAjax()
+ );
+ foreach ( [ 'success', 'message', 'error' ] as $item ) {
+ if ( isset( $response->action_response_data[ $item ] ) ) {
+ $response->{$item} = $response->action_response_data[ $item ];
+ }
+ }
+
+ $this->setResponse( $response );
+ }
+
+ protected function getParamsMinusAjax() :array {
+ return array_diff_key(
+ $this->action_data,
+ array_flip( [
+ ActionData::FIELD_ACTION,
+ ActionData::FIELD_EXECUTE,
+ ActionData::FIELD_NONCE,
+ ActionData::FIELD_WRAP_RESPONSE,
+ 'render_slug'
+ ] )
+ );
+ }
+
+ protected function getRequiredDataKeys() :array {
+ return [
+ 'render_slug'
+ ];
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BaseAction.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BaseAction.php
@@ -1,161 +1,184 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
-
-use FernleafSystemsUtilitiesDataAdapterDynPropertiesClass;
-use FernleafSystemsWordpressPluginShieldActionRouterActionNonce;
-use FernleafSystemsWordpressPluginShieldActionRouterActionResponse;
-use FernleafSystemsWordpressPluginShieldActionRouterConstants;
-use FernleafSystemsWordpressPluginShieldActionRouterExceptions{
- InvalidActionNonceException,
- IpBlockedException,
- SecurityAdminRequiredException,
- UserAuthRequiredException
-};
-use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
-use FernleafSystemsWordpressPluginShieldModulesPluginControllerConsumer;
-use FernleafSystemsWordpressServicesServices;
-
-/**
- * @property array $action_data
- */
-abstract class BaseAction extends DynPropertiesClass {
-
- use PluginControllerConsumer;
-
- public const SLUG = '';
-
- private ActionResponse $response;
-
- public function __construct( array $data = [], ?ActionResponse $response = null ) {
- $this->action_data = $data;
- $this->response = $response instanceof ActionResponse ? $response : new ActionResponse();
- }
-
- public function __get( string $key ) {
- $value = parent::__get( $key );
-
- switch ( $key ) {
- case 'action_data':
- $value = array_merge( $this->getDefaults(), is_array( $value ) ? $value : [] );
- break;
- default:
- break;
- }
-
- return $value;
- }
-
- /**
- * @throws ActionException
- * @throws InvalidActionNonceException
- * @throws IpBlockedException
- * @throws SecurityAdminRequiredException
- * @throws UserAuthRequiredException
- */
- public function process() {
- $this->checkAccess();
- $this->checkAvailableData();
- $this->preExec();
- $this->exec();
- $this->postExec();
- }
-
- /**
- * @throws InvalidActionNonceException
- * @throws IpBlockedException
- * @throws SecurityAdminRequiredException
- * @throws UserAuthRequiredException
- */
- protected function checkAccess() {
- $con = self::con();
- $thisReq = $con->this_req;
- if ( !$thisReq->request_bypasses_all_restrictions && $thisReq->is_ip_blocked && !$this->canBypassIpAddressBlock() ) {
- throw new IpBlockedException( sprintf( 'IP Address blocked so cannot process action: %s', static::SLUG ) );
- }
-
- $WPU = Services::WpUsers();
- if ( $this->isUserAuthRequired()
- && ( !$WPU->isUserLoggedIn() || !user_can( $WPU->getCurrentWpUser(), $this->getMinimumUserAuthCapability() ) ) ) {
- throw new UserAuthRequiredException( sprintf( 'Must be logged-in to execute this action: %s', static::SLUG ) );
- }
-
- if ( !$thisReq->is_security_admin && $this->isSecurityAdminRequired() ) {
- throw new SecurityAdminRequiredException( sprintf( 'Security admin required for action: %s', static::SLUG ) );
- }
-
- if ( $this->isNonceVerifyRequired() && !ActionNonce::VerifyFromRequest() ) {
- throw new InvalidActionNonceException( 'Invalid Action Nonce Exception.' );
- }
- }
-
- protected function preExec() {
- }
-
- protected function postExec() {
- }
-
- /**
- * @throws ActionException
- */
- abstract protected function exec();
-
- public function response() :ActionResponse {
- $this->response->action_slug = static::SLUG;
- $this->response->action_data = $this->action_data;
- return $this->response;
- }
-
- public function setResponse( ActionResponse $response ) {
- $this->response = $response;
- }
-
- protected function getDefaults() :array {
- return [];
- }
-
- protected function getMinimumUserAuthCapability() :string {
- return self::con()->cfg->properties[ 'base_permissions' ] ?? 'manage_options';
- }
-
- protected function canBypassIpAddressBlock() :bool {
- return false;
- }
-
- protected function isNonceVerifyRequired() :bool {
- return (bool)( $this->getActionOverrides()[ Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED ] ?? self::con()->this_req->wp_is_ajax );
- }
-
- protected function isUserAuthRequired() :bool {
- return !empty( $this->getMinimumUserAuthCapability() );
- }
-
- protected function isSecurityAdminRequired() :bool {
- return $this->getMinimumUserAuthCapability() === 'manage_options';
- }
-
- protected function getActionOverrides() :array {
- return $this->action_data[ 'action_overrides' ] ?? [];
- }
-
- /**
- * @throws ActionException
- */
- protected function checkAvailableData() {
- $missing = array_diff( array_unique( $this->getRequiredDataKeys() ), array_keys( $this->action_data ) );
- if ( !empty( $missing ) ) {
- throw new ActionException( sprintf( 'Missing action (%s) data for the following keys: %s', static::SLUG, implode( ', ', $missing ) ) );
- }
- }
-
- protected function getRequiredDataKeys() :array {
- return [];
- }
-
- public static function NonceCfg() :array {
- return [
- 'ip' => false,
- 'ttl' => 12,
- ];
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsUtilitiesDataAdapterDynPropertiesClass;
+use FernleafSystemsWordpressPluginShieldActionRouterActionNonce;
+use FernleafSystemsWordpressPluginShieldActionRouterActionResponse;
+use FernleafSystemsWordpressPluginShieldActionRouterConstants;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptions{
+ InvalidActionNonceException,
+ IpBlockedException,
+ SecurityAdminRequiredException,
+ UserAuthRequiredException
+};
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
+use FernleafSystemsWordpressPluginShieldModulesPluginControllerConsumer;
+use FernleafSystemsWordpressServicesServices;
+
+/**
+ * @property array $action_data
+ */
+abstract class BaseAction extends DynPropertiesClass {
+
+ use PluginControllerConsumer;
+
+ public const SLUG = '';
+
+ private ActionResponse $response;
+
+ public function __construct( array $data = [], ?ActionResponse $response = null ) {
+ $this->action_data = $data;
+ $this->response = $response instanceof ActionResponse ? $response : new ActionResponse();
+ }
+
+ public function __get( string $key ) {
+ $value = parent::__get( $key );
+
+ switch ( $key ) {
+ case 'action_data':
+ $value = array_merge( $this->getDefaults(), is_array( $value ) ? $value : [] );
+ break;
+ default:
+ break;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @throws ActionException
+ * @throws InvalidActionNonceException
+ * @throws IpBlockedException
+ * @throws SecurityAdminRequiredException
+ * @throws UserAuthRequiredException
+ */
+ public function process() {
+ $this->checkAccess();
+ $this->checkAvailableData();
+ $this->preExec();
+ $this->exec();
+ $this->postExec();
+ }
+
+ /**
+ * @throws InvalidActionNonceException
+ * @throws IpBlockedException
+ * @throws SecurityAdminRequiredException
+ * @throws UserAuthRequiredException
+ */
+ protected function checkAccess() {
+ $con = self::con();
+ $thisReq = $con->this_req;
+ if ( !$thisReq->request_bypasses_all_restrictions && $thisReq->is_ip_blocked && !$this->canBypassIpAddressBlock() ) {
+ throw new IpBlockedException( sprintf( 'IP Address blocked so cannot process action: %s', static::SLUG ) );
+ }
+
+ $WPU = Services::WpUsers();
+ if ( $this->isUserAuthRequired()
+ && ( !$WPU->isUserLoggedIn() || !user_can( $WPU->getCurrentWpUser(), $this->getMinimumUserAuthCapability() ) ) ) {
+ throw new UserAuthRequiredException( sprintf( 'Must be logged-in to execute this action: %s', static::SLUG ) );
+ }
+
+ if ( !$thisReq->is_security_admin && $this->isSecurityAdminRequired() ) {
+ throw new SecurityAdminRequiredException( sprintf( 'Security admin required for action: %s', static::SLUG ) );
+ }
+
+ if ( $this->isNonceVerifyRequired() && !ActionNonce::VerifyFromRequest() ) {
+ throw new InvalidActionNonceException( 'Invalid Action Nonce Exception.' );
+ }
+ }
+
+ protected function preExec() {
+ }
+
+ protected function postExec() {
+ }
+
+ /**
+ * @throws ActionException
+ */
+ abstract protected function exec();
+
+ public function response() :ActionResponse {
+ $this->response->action_slug = static::SLUG;
+ $this->response->action_data = $this->action_data;
+ return $this->response;
+ }
+
+ public function setResponse( ActionResponse $response ) {
+ $this->response = $response;
+ }
+
+ protected function getDefaults() :array {
+ return [];
+ }
+
+ protected function getMinimumUserAuthCapability() :string {
+ return self::con()->cfg->properties[ 'base_permissions' ] ?? 'manage_options';
+ }
+
+ protected function canBypassIpAddressBlock() :bool {
+ return false;
+ }
+
+ protected function isNonceVerifyRequired() :bool {
+ return (bool)( $this->getActionOverrides()[ Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED ] ?? self::con()->this_req->wp_is_ajax );
+ }
+
+ protected function isUserAuthRequired() :bool {
+ return !empty( $this->getMinimumUserAuthCapability() );
+ }
+
+ protected function isSecurityAdminRequired() :bool {
+ return $this->getMinimumUserAuthCapability() === 'manage_options';
+ }
+
+ protected function getActionOverrides() :array {
+ return $this->action_data[ 'action_overrides' ] ?? [];
+ }
+
+ /**
+ * Set action override programmatically (for trusted integrations like MainWP)
+ *
+ * This method allows trusted integrations to set action overrides AFTER action creation,
+ * ensuring security controls never come from user input paths.
+ *
+ * SECURITY NOTE: This method should ONLY be called in trusted contexts where:
+ * - The caller has verified authentication (e.g., MainWP authenticated requests)
+ * - The override is set programmatically, not from user input
+ * - The action object has already been created via ActionProcessor::getAction()
+ *
+ * @param string $overrideKey Override key constant (e.g., Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED)
+ * @param mixed $value Override value (typically boolean for is_nonce_verify_required)
+ * @return self For method chaining
+ */
+ public function setActionOverride( string $overrideKey, $value ) :self {
+ $this->action_data[ 'action_overrides' ] = array_merge(
+ is_array( $this->action_data[ 'action_overrides' ] ?? null ) ? $this->action_data[ 'action_overrides' ] : [],
+ [ $overrideKey => $value ]
+ );
+ return $this;
+ }
+
+ /**
+ * @throws ActionException
+ */
+ protected function checkAvailableData() {
+ $missing = array_diff( array_unique( $this->getRequiredDataKeys() ), array_keys( $this->action_data ) );
+ if ( !empty( $missing ) ) {
+ throw new ActionException( sprintf( 'Missing action (%s) data for the following keys: %s', static::SLUG, implode( ', ', $missing ) ) );
+ }
+ }
+
+ protected function getRequiredDataKeys() :array {
+ return [];
+ }
+
+ public static function NonceCfg() :array {
+ return [
+ 'ip' => false,
+ 'ttl' => 12,
+ ];
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BlockdownDisableFormSubmit.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BlockdownDisableFormSubmit.php
@@ -1,56 +1,56 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
-
-use FernleafSystemsWordpressPluginShieldModulesIPsLibIpRules;
-use FernleafSystemsWordpressPluginShieldModulesPluginLibSiteLockdownSiteBlockdownCfg;
-use FernleafSystemsWordpressServicesServices;
-
-class BlockdownDisableFormSubmit extends BaseAction {
-
- public const SLUG = 'blockdown_disable_form_submit';
-
- protected function exec() {
- $con = self::con();
- try {
- $cfg = ( new SiteBlockdownCfg() )->applyFromArray( $con->comps->opts_lookup->getBlockdownCfg() );
-
- if ( !$cfg->isLockdownActive() ) {
- throw new Exception( 'Invalid request - lockdown is not active.' );
- }
-
- $cfg->disabled_at = Services::Request()->ts();
- $cfg->exclusions = [];
-
- if ( $cfg->whitelist_me ) {
- $status = new IpRulesIpRuleStatus( $cfg->whitelist_me );
- if ( $status->isBypass() ) {
- $ipRules = $status->getRulesForBypass();
- foreach ( $ipRules as $ipRule ) {
- if ( !$ipRule->is_range && $ipRule->ip === $cfg->whitelist_me ) {
- ( new IpRulesDeleteRule() )->byRecord( $ipRule );
- }
- }
- }
- }
- $cfg->whitelist_me = '';
- $con->opts->optSet( 'blockdown_cfg', $cfg->getRawData() );
- $con->fireEvent( 'site_blockdown_ended', [
- 'audit_params' => [ 'user_login' => Services::WpUsers()->getCurrentWpUsername() ]
- ] );
-
- $msg = __( 'Site lock down has been lifted!', 'wp-simple-firewall' );
- $success = true;
- }
- catch ( Exception $e ) {
- $success = false;
- $msg = $e->getMessage();
- }
-
- $this->response()->action_response_data = [
- 'success' => $success,
- 'page_reload' => true,
- 'message' => $msg,
- ];
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsWordpressPluginShieldModulesIPsLibIpRules;
+use FernleafSystemsWordpressPluginShieldModulesPluginLibSiteLockdownSiteBlockdownCfg;
+use FernleafSystemsWordpressServicesServices;
+
+class BlockdownDisableFormSubmit extends BaseAction {
+
+ public const SLUG = 'blockdown_disable_form_submit';
+
+ protected function exec() {
+ $con = self::con();
+ try {
+ $cfg = ( new SiteBlockdownCfg() )->applyFromArray( $con->comps->opts_lookup->getBlockdownCfg() );
+
+ if ( !$cfg->isLockdownActive() ) {
+ throw new Exception( 'Invalid request - lockdown is not active.' );
+ }
+
+ $cfg->disabled_at = Services::Request()->ts();
+ $cfg->exclusions = [];
+
+ if ( $cfg->whitelist_me ) {
+ $status = new IpRulesIpRuleStatus( $cfg->whitelist_me );
+ if ( $status->isBypass() ) {
+ $ipRules = $status->getRulesForBypass();
+ foreach ( $ipRules as $ipRule ) {
+ if ( !$ipRule->is_range && $ipRule->ip === $cfg->whitelist_me ) {
+ ( new IpRulesDeleteRule() )->byRecord( $ipRule );
+ }
+ }
+ }
+ }
+ $cfg->whitelist_me = '';
+ $con->opts->optSet( 'blockdown_cfg', $cfg->getRawData() );
+ $con->fireEvent( 'site_blockdown_ended', [
+ 'audit_params' => [ 'user_login' => Services::WpUsers()->getCurrentWpUsername() ]
+ ] );
+
+ $msg = __( 'Site lock down has been lifted!', 'wp-simple-firewall' );
+ $success = true;
+ }
+ catch ( Exception $e ) {
+ $success = false;
+ $msg = $e->getMessage();
+ }
+
+ $this->response()->action_response_data = [
+ 'success' => $success,
+ 'page_reload' => true,
+ 'message' => $msg,
+ ];
+ }
}
No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BlockdownFormSubmit.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BlockdownFormSubmit.php
@@ -1,78 +1,78 @@
-<?php declare( strict_types=1 );
-
-namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
-
-use FernleafSystemsWordpressPluginShieldDBsIpRulesLoadIpRules;
-use FernleafSystemsWordpressPluginShieldModulesIPsLibIpRules;
-use FernleafSystemsWordpressPluginShieldModulesPluginLibSiteLockdownSiteBlockdownCfg;
-use FernleafSystemsWordpressServicesServices;
-
-class BlockdownFormSubmit extends BaseAction {
-
- public const SLUG = 'blockdown_form_submit';
-
- protected function exec() {
- $con = self::con();
-
- $form = $this->action_data[ 'form_data' ];
- try {
- if ( !$con->caps->canSiteBlockdown() ) {
- throw new Exception( 'Please upgrade your ShieldPRO plan to make use of this feature.' );
- }
-
- if ( empty( $form ) || !is_array( $form ) ) {
- throw new Exception( 'Please complete the form.' );
- }
-
- $cfg = ( new SiteBlockdownCfg() )->applyFromArray( $con->comps->opts_lookup->getBlockdownCfg() );
- if ( $cfg->isLockdownActive() ) {
- throw new Exception( 'Invalid request - lockdown is already active.' );
- }
-
- $confirm = $form[ 'confirm' ] ?? [];
- if ( !empty( array_diff( [ 'consequences', 'authority', 'access', 'cache' ], $confirm ) ) ) {
- throw new Exception( 'Please check all confirmation boxes.' );
- }
-
- $whitelistMe = ( $form[ 'whitelist_me' ] ?? 'N' ) === 'Y';
- $alreadyWhitelisted = ( new IpRulesIpRuleStatus( $con->this_req->ip ) )->isBypass();
- if ( $whitelistMe && !$alreadyWhitelisted ) {
- ( new IpRulesAddRule() )
- ->setIP( $con->this_req->ip )
- ->toManualWhitelist( 'Whitelist for Site Lockdown' );
- }
-
- $ruleLoader = new LoadIpRules();
- $ruleLoader->wheres = [
- sprintf( "`ir`.`type`='%s'", $con->db_con->ip_rules::T_MANUAL_BYPASS )
- ];
- if ( $ruleLoader->countAll() === 0 ) {
- throw new Exception( 'There are no whitelisted IPs for exclusion.' );
- }
-
- $cfg->activated_at = Services::Request()->ts();
- $cfg->activated_by = Services::WpUsers()->getCurrentWpUsername();
- $cfg->exclusions = $form[ 'exclusions' ] ?? [];
- $cfg->whitelist_me = ( $whitelistMe && !$alreadyWhitelisted ) ? $con->this_req->ip : '';
-
- $con->opts->optSet( 'blockdown_cfg', $cfg->getRawData() );
-
- self::con()->fireEvent( 'site_blockdown_started', [
- 'audit_params' => [ 'user_login' => Services::WpUsers()->getCurrentWpUsername() ]
- ] );
-
- $msg = __( 'Site has been locked down!', 'wp-simple-firewall' );
- $success = true;
- }
- catch ( Exception $e ) {
- $success = false;
- $msg = $e->getMessage();
- }
-
- $this->response()->action_response_data = [
- 'success' => $success,
- 'page_reload' => $success,
- 'message' => $msg,
- ];
- }
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsWordpressPluginShieldDBsIpRulesLoadIpRules;
+use FernleafSystemsWordpressPluginShieldModulesIPsLibIpRules;
+use FernleafSystemsWordpressPluginShieldModulesPluginLibSiteLockdownSiteBlockdownCfg;
+use FernleafSystemsWordpressServicesServices;
+
+class BlockdownFormSubmit extends BaseAction {
+
+ public const SLUG = 'blockdown_form_submit';
+
+ protected function exec() {
+ $con = self::con();
+
+ $form = $this->action_data[ 'form_data' ];
+ try {
+ if ( !$con->caps->canSiteBlockdown() ) {
+ throw new Exception( 'Please upgrade your ShieldPRO plan to make use of this feature.' );
+ }
+
+ if ( empty( $form ) || !is_array( $form ) ) {
+ throw new Exception( 'Please complete the form.' );
+ }
+
+ $cfg = ( new SiteBlockdownCfg() )->applyFromArray( $con->comps->opts_lookup->getBlockdownCfg() );
+ if ( $cfg->isLockdownActive() ) {
+ throw new Exception( 'Invalid reques