| @@ -1,217 +1,194 @@ | ||
| 1 | -<?php | |
| 2 | - | |
| 3 | -// Do not allow the file to be called directly. | |
| 4 | -if ( ! defined( 'ABSPATH' ) ) { | |
| 5 | - exit; | |
| 6 | -} | |
| 7 | - | |
| 8 | -/** | |
| 9 | - * This class provides the firewall functionality. | |
| 10 | - */ | |
| 11 | -class P_Firewall extends P_Core { | |
| 12 | - | |
| 13 | - /** | |
| 14 | - * Launch the firewall rule processor. | |
| 15 | - * | |
| 16 | - * @param bool $from_main Whether or not the firewall is loaded from the main script or not. | |
| 17 | - * @param Patchstack $core | |
| 18 | - * @param bool $skip Whether or not to process and execute the rules. | |
| 19 | - * @param bool $muCall Whether or not this was called from mu-plugin. | |
| 20 | - * @return void | |
| 21 | - */ | |
| 22 | - public function __construct( $from_main = false, $core = null, $skip = false, $muCall = false ) { | |
| 23 | - if ( ! $from_main || ! $core ) { | |
| 24 | - if ( $core ) { | |
| 25 | - parent::__construct( $core ); | |
| 26 | - } | |
| 27 | - return; | |
| 28 | - } | |
| 29 | - | |
| 30 | - parent::__construct( $core ); | |
| 31 | - | |
| 32 | - // If we only want to initialize the firewall but not execute the rules. | |
| 33 | - if ( $skip || defined( 'DOING_CRON' ) ) { | |
| 34 | - return; | |
| 35 | - } | |
| 36 | - | |
| 37 | - // Load the extension. | |
| 38 | - require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php'; | |
| 39 | - $extension = new Patchstack\Extensions\WordPress\Extension( | |
| 40 | - [ | |
| 41 | - 'patchstack_basic_firewall_roles' => $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] ), | |
| 42 | - 'patchstack_whitelist' => get_option( 'patchstack_whitelist', '' ) | |
| 43 | - ], | |
| 44 | - $this | |
| 45 | - ); | |
| 46 | - | |
| 47 | - // Initiate the firewall processor with our settings. | |
| 48 | - $firewall = new Patchstack\Processor( | |
| 49 | - $extension, | |
| 50 | - $this->decode_rule_option('patchstack_firewall_rules_v3'), | |
| 51 | - $this->decode_rule_option('patchstack_whitelist_rules_v3'), | |
| 52 | - [ | |
| 53 | - 'autoblockAttempts' => $this->get_option( 'patchstack_autoblock_attempts', 10 ), | |
| 54 | - 'autoblockMinutes' => $this->get_option( 'patchstack_autoblock_minutes', 30 ), | |
| 55 | - 'autoblockTime' => $this->get_option( 'patchstack_autoblock_blocktime', 60 ), | |
| 56 | - 'whitelistKeysRules' => $this->decode_rule_option( 'patchstack_whitelist_keys_rules' ), | |
| 57 | - 'mustUsePluginCall' => $muCall | |
| 58 | - ], | |
| 59 | - $this->decode_rule_option('patchstack_firewall_rules'), | |
| 60 | - $this->decode_rule_option('patchstack_whitelist_rules') | |
| 61 | - ); | |
| 62 | - | |
| 63 | - // Launch the firewall. | |
| 64 | - $firewall->launch(); | |
| 65 | - } | |
| 66 | - | |
| 67 | - /** | |
| 68 | - * Safely decode a stored rule option into an array. The option is expected to be | |
| 69 | - * a JSON string, but may already be an array (or a malformed value); passing a | |
| 70 | - * non-string to json_decode() is a fatal TypeError on PHP 8. | |
| 71 | - * | |
| 72 | - * @param string $name | |
| 73 | - * @return array | |
| 74 | - */ | |
| 75 | - private function decode_rule_option( $name ) { | |
| 76 | - $value = get_option( $name, '[]' ); | |
| 77 | - | |
| 78 | - if ( is_array( $value ) ) { | |
| 79 | - return $value; | |
| 80 | - } | |
| 81 | - | |
| 82 | - if ( ! is_string( $value ) ) { | |
| 83 | - return []; | |
| 84 | - } | |
| 85 | - | |
| 86 | - $decoded = json_decode( $value, true ); | |
| 87 | - return is_array( $decoded ) ? $decoded : []; | |
| 88 | - } | |
| 89 | - | |
| 90 | - /** | |
| 91 | - * Determine if the user is authenticated and in the list of whitelisted roles. | |
| 92 | - * | |
| 93 | - * @return bool | |
| 94 | - */ | |
| 95 | - public function is_authenticated() { | |
| 96 | - if ( ! is_user_logged_in() ) { | |
| 97 | - return false; | |
| 98 | - } | |
| 99 | - | |
| 100 | - // Get the whitelisted roles. | |
| 101 | - $roles = $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] ); | |
| 102 | - if ( ! is_array( $roles ) ) { | |
| 103 | - return false; | |
| 104 | - } | |
| 105 | - | |
| 106 | - // Special scenario for super admins on a multisite environment. | |
| 107 | - if ( in_array( 'administrator', $roles ) && is_multisite() && is_super_admin() ) { | |
| 108 | - return true; | |
| 109 | - } | |
| 110 | - | |
| 111 | - // Get the roles of the user. | |
| 112 | - $user = wp_get_current_user(); | |
| 113 | - if ( ! isset( $user->roles ) || count( (array) $user->roles ) == 0 ) { | |
| 114 | - return false; | |
| 115 | - } | |
| 116 | - | |
| 117 | - // Is the user in the whitelist roles list? | |
| 118 | - $role_count = array_intersect( $user->roles, $roles ); | |
| 119 | - return count( $role_count ) != 0; | |
| 120 | - } | |
| 121 | - | |
| 122 | - /** | |
| 123 | - * Display error page. | |
| 124 | - * | |
| 125 | - * @param integer $fid | |
| 126 | - * @return void | |
| 127 | - */ | |
| 128 | - public function display_error_page( $fid = 1 ) { | |
| 129 | - if ( $fid != 22 && $fid != 23 && $fid != 24 && $fid != 'login' ) { | |
| 130 | - $this->log_request( $fid ); | |
| 131 | - } | |
| 132 | - | |
| 133 | - // Supported by a number of popular caching plugins. | |
| 134 | - if ( ! defined( 'DONOTCACHEPAGE' ) ) { | |
| 135 | - define( 'DONOTCACHEPAGE', true ); | |
| 136 | - } | |
| 137 | - | |
| 138 | - // Because WP Fastest Cache just has to be special... | |
| 139 | - if (function_exists('wpfc_exclude_current_page')) { | |
| 140 | - @wpfc_exclude_current_page(); | |
| 141 | - } | |
| 142 | - | |
| 143 | - // Send forbidden headers and no-caching headers as well. | |
| 144 | - status_header(403); | |
| 145 | - send_nosniff_header(); | |
| 146 | - nocache_headers(); | |
| 147 | - | |
| 148 | - if ( $fid == 'login' ) { | |
| 149 | - require_once dirname( __FILE__ ) . '/views/access-denied-login.php'; | |
| 150 | - } else { | |
| 151 | - require_once dirname( __FILE__ ) . '/views/access-denied.php'; | |
| 152 | - } | |
| 153 | - | |
| 154 | - exit; | |
| 155 | - } | |
| 156 | - | |
| 157 | - /** | |
| 158 | - * Log the blocked request. | |
| 159 | - * | |
| 160 | - * @param int $fid | |
| 161 | - * @return void | |
| 162 | - */ | |
| 163 | - private function log_request( $fid = 1 ) { | |
| 164 | - global $wpdb; | |
| 165 | - if ( ! $wpdb || $fid == 22 || $fid == 23 || $fid == 24 || $fid == 'login' ) { | |
| 166 | - return; | |
| 167 | - } | |
| 168 | - | |
| 169 | - // Insert into the logs. | |
| 170 | - $wpdb->insert( | |
| 171 | - $wpdb->prefix . 'patchstack_firewall_log', | |
| 172 | - array( | |
| 173 | - 'ip' => $this->get_ip(), | |
| 174 | - 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '', | |
| 175 | - 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '', | |
| 176 | - 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '', | |
| 177 | - 'fid' => $fid, | |
| 178 | - 'flag' => '', | |
| 179 | - 'post_data' => '', | |
| 180 | - 'block_type' => 'BLOCK', | |
| 181 | - ) | |
| 182 | - ); | |
| 183 | - } | |
| 184 | - | |
| 185 | - /** | |
| 186 | - * Extract the number of blocked hits the past 30 days based on the current counters. | |
| 187 | - * | |
| 188 | - * @return int | |
| 189 | - */ | |
| 190 | - public function get_hits_counter() { | |
| 191 | - $counters = get_option( 'patchstack_hits_last_30', [] ); | |
| 192 | - if (!is_array($counters) || count($counters) === 0) { | |
| 193 | - return 0; | |
| 194 | - } | |
| 195 | - | |
| 196 | - // Set the range of dates we need. | |
| 197 | - $hits = 0; | |
| 198 | - $start = new \DateTime(); | |
| 199 | - $start->modify('-30 days'); | |
| 200 | - | |
| 201 | - $end = new \DateTime(); | |
| 202 | - $end->modify('+1 day'); | |
| 203 | - | |
| 204 | - $interval = new \DateInterval('P1D'); | |
| 205 | - $range = new \DatePeriod($start, $interval, $end); | |
| 206 | - | |
| 207 | - // Set the range from -6 days to +1 day from now. | |
| 208 | - foreach ($range as $date) { | |
| 209 | - $formattedDate = $date->format('Y-m-d'); | |
| 210 | - if (isset($counters[$formattedDate])) { | |
| 211 | - $hits += $counters[$formattedDate]; | |
| 212 | - } | |
| 213 | - } | |
| 214 | - | |
| 215 | - return $hits; | |
| 216 | - } | |
| 217 | -} | |
| 1 | +<?php | |
| 2 | + | |
| 3 | +// Do not allow the file to be called directly. | |
| 4 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 5 | + exit; | |
| 6 | +} | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * This class provides the firewall functionality. | |
| 10 | + */ | |
| 11 | +class P_Firewall extends P_Core { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * Launch the firewall rule processor. | |
| 15 | + * | |
| 16 | + * @param bool $from_main Whether or not the firewall is loaded from the main script or not. | |
| 17 | + * @param Patchstack $core | |
| 18 | + * @param bool $skip Whether or not to process and execute the rules. | |
| 19 | + * @param bool $muCall Whether or not this was called from mu-plugin. | |
| 20 | + * @return void | |
| 21 | + */ | |
| 22 | + public function __construct( $from_main = false, $core = null, $skip = false, $muCall = false ) { | |
| 23 | + if ( ! $from_main || ! $core ) { | |
| 24 | + if ( $core ) { | |
| 25 | + parent::__construct( $core ); | |
| 26 | + } | |
| 27 | + return; | |
| 28 | + } | |
| 29 | + | |
| 30 | + parent::__construct( $core ); | |
| 31 | + | |
| 32 | + // If we only want to initialize the firewall but not execute the rules. | |
| 33 | + if ( $skip || defined( 'DOING_CRON' ) ) { | |
| 34 | + return; | |
| 35 | + } | |
| 36 | + | |
| 37 | + // Load the extension. | |
| 38 | + require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php'; | |
| 39 | + $extension = new Patchstack\Extensions\WordPress\Extension( | |
| 40 | + [ | |
| 41 | + 'patchstack_basic_firewall_roles' => $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] ), | |
| 42 | + 'patchstack_whitelist' => get_option( 'patchstack_whitelist', '' ) | |
| 43 | + ], | |
| 44 | + $this | |
| 45 | + ); | |
| 46 | + | |
| 47 | + // Initiate the firewall processor with our settings. | |
| 48 | + $firewall = new Patchstack\Processor( | |
| 49 | + $extension, | |
| 50 | + json_decode(get_option('patchstack_firewall_rules_v3', '[]'), true), | |
| 51 | + json_decode(get_option('patchstack_whitelist_rules_v3', '[]'), true), | |
| 52 | + [ | |
| 53 | + 'autoblockAttempts' => $this->get_option( 'patchstack_autoblock_attempts', 10 ), | |
| 54 | + 'autoblockMinutes' => $this->get_option( 'patchstack_autoblock_minutes', 30 ), | |
| 55 | + 'autoblockTime' => $this->get_option( 'patchstack_autoblock_blocktime', 60 ), | |
| 56 | + 'whitelistKeysRules' => json_decode( get_option( 'patchstack_whitelist_keys_rules', '[]' ), true ), | |
| 57 | + 'mustUsePluginCall' => $muCall | |
| 58 | + ], | |
| 59 | + json_decode(get_option('patchstack_firewall_rules', '[]'), true), | |
| 60 | + json_decode(get_option('patchstack_whitelist_rules', '[]'), true) | |
| 61 | + ); | |
| 62 | + | |
| 63 | + // Launch the firewall. | |
| 64 | + $firewall->launch(); | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * Determine if the user is authenticated and in the list of whitelisted roles. | |
| 69 | + * | |
| 70 | + * @return bool | |
| 71 | + */ | |
| 72 | + public function is_authenticated() { | |
| 73 | + if ( ! is_user_logged_in() ) { | |
| 74 | + return false; | |
| 75 | + } | |
| 76 | + | |
| 77 | + // Get the whitelisted roles. | |
| 78 | + $roles = $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] ); | |
| 79 | + if ( ! is_array( $roles ) ) { | |
| 80 | + return false; | |
| 81 | + } | |
| 82 | + | |
| 83 | + // Special scenario for super admins on a multisite environment. | |
| 84 | + if ( in_array( 'administrator', $roles ) && is_multisite() && is_super_admin() ) { | |
| 85 | + return true; | |
| 86 | + } | |
| 87 | + | |
| 88 | + // Get the roles of the user. | |
| 89 | + $user = wp_get_current_user(); | |
| 90 | + if ( ! isset( $user->roles ) || count( (array) $user->roles ) == 0 ) { | |
| 91 | + return false; | |
| 92 | + } | |
| 93 | + | |
| 94 | + // Is the user in the whitelist roles list? | |
| 95 | + $role_count = array_intersect( $user->roles, $roles ); | |
| 96 | + return count( $role_count ) != 0; | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 100 | + * Display error page. | |
| 101 | + * | |
| 102 | + * @param integer $fid | |
| 103 | + * @return void | |
| 104 | + */ | |
| 105 | + public function display_error_page( $fid = 1 ) { | |
| 106 | + if ( $fid != 22 && $fid != 23 && $fid != 24 && $fid != 'login' ) { | |
| 107 | + $this->log_request( $fid ); | |
| 108 | + } | |
| 109 | + | |
| 110 | + // Supported by a number of popular caching plugins. | |
| 111 | + if ( ! defined( 'DONOTCACHEPAGE' ) ) { | |
| 112 | + define( 'DONOTCACHEPAGE', true ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + // Because WP Fastest Cache just has to be special... | |
| 116 | + if (function_exists('wpfc_exclude_current_page')) { | |
| 117 | + @wpfc_exclude_current_page(); | |
| 118 | + } | |
| 119 | + | |
| 120 | + // Send forbidden headers and no-caching headers as well. | |
| 121 | + status_header(403); | |
| 122 | + send_nosniff_header(); | |
| 123 | + nocache_headers(); | |
| 124 | + | |
| 125 | + if ( $fid == 'login' ) { | |
| 126 | + require_once dirname( __FILE__ ) . '/views/access-denied-login.php'; | |
| 127 | + } else { | |
| 128 | + require_once dirname( __FILE__ ) . '/views/access-denied.php'; | |
| 129 | + } | |
| 130 | + | |
| 131 | + exit; | |
| 132 | + } | |
| 133 | + | |
| 134 | + /** | |
| 135 | + * Log the blocked request. | |
| 136 | + * | |
| 137 | + * @param int $fid | |
| 138 | + * @return void | |
| 139 | + */ | |
| 140 | + private function log_request( $fid = 1 ) { | |
| 141 | + global $wpdb; | |
| 142 | + if ( ! $wpdb || $fid == 22 || $fid == 23 || $fid == 24 || $fid == 'login' ) { | |
| 143 | + return; | |
| 144 | + } | |
| 145 | + | |
| 146 | + // Insert into the logs. | |
| 147 | + $wpdb->insert( | |
| 148 | + $wpdb->prefix . 'patchstack_firewall_log', | |
| 149 | + array( | |
| 150 | + 'ip' => $this->get_ip(), | |
| 151 | + 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '', | |
| 152 | + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '', | |
| 153 | + 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '', | |
| 154 | + 'fid' => $fid, | |
| 155 | + 'flag' => '', | |
| 156 | + 'post_data' => '', | |
| 157 | + 'block_type' => 'BLOCK', | |
| 158 | + ) | |
| 159 | + ); | |
| 160 | + } | |
| 161 | + | |
| 162 | + /** | |
| 163 | + * Extract the number of blocked hits the past 30 days based on the current counters. | |
| 164 | + * | |
| 165 | + * @return int | |
| 166 | + */ | |
| 167 | + public function get_hits_counter() { | |
| 168 | + $counters = get_option( 'patchstack_hits_last_30', [] ); | |
| 169 | + if (!is_array($counters) || count($counters) === 0) { | |
| 170 | + return 0; | |
| 171 | + } | |
| 172 | + | |
| 173 | + // Set the range of dates we need. | |
| 174 | + $hits = 0; | |
| 175 | + $start = new \DateTime(); | |
| 176 | + $start->modify('-30 days'); | |
| 177 | + | |
| 178 | + $end = new \DateTime(); | |
| 179 | + $end->modify('+1 day'); | |
| 180 | + | |
| 181 | + $interval = new \DateInterval('P1D'); | |
| 182 | + $range = new \DatePeriod($start, $interval, $end); | |
| 183 | + | |
| 184 | + // Set the range from -6 days to +1 day from now. | |
| 185 | + foreach ($range as $date) { | |
| 186 | + $formattedDate = $date->format('Y-m-d'); | |
| 187 | + if (isset($counters[$formattedDate])) { | |
| 188 | + $hits += $counters[$formattedDate]; | |
| 189 | + } | |
| 190 | + } | |
| 191 | + | |
| 192 | + return $hits; | |
| 193 | + } | |
| 194 | +} | |