| @@ -18,10 +18,10 @@ | ||
| 18 | 18 | * 'Scripts Presets' is associated with the properties $script_presets & $script_preset_values, |
| 19 | 19 | * 'Custom User Agents' is associated with the property $blacklist. |
| 20 | 20 | */ |
| 21 | 21 | class User_Agent_Lockout extends Setting { |
| 22 | - const BOT_TRAP_LOCKOUT_TYPE_ALLOWED = array( 'temporary', 'permanent' ); | |
| 23 | - const BOT_TRAP_LOCKOUT_DURATION_UNIT_ALLOWED = array( 'seconds', 'minutes', 'hours' ); | |
| 22 | + const BOT_LOCKOUT_TYPE_ALLOWED = array( 'temporary', 'permanent' ); | |
| 23 | + const BOT_LOCKOUT_DURATION_UNIT_ALLOWED = array( 'seconds', 'minutes', 'hours' ); | |
| 24 | 24 | |
| 25 | 25 | /** |
| 26 | 26 | * Option name. |
| 27 | 27 | * |
| @@ -34,9 +34,9 @@ | ||
| 34 | 34 | * |
| 35 | 35 | * @var bool |
| 36 | 36 | * @defender_property |
| 37 | 37 | */ |
| 38 | - public $enabled = false; | |
| 38 | + public bool $enabled = false; | |
| 39 | 39 | |
| 40 | 40 | /** |
| 41 | 41 | * Blacklist User Agents. |
| 42 | 42 | * |
| @@ -102,14 +102,14 @@ | ||
| 102 | 102 | */ |
| 103 | 103 | public $script_preset_values = array(); |
| 104 | 104 | |
| 105 | 105 | /** |
| 106 | - * Is bot trap enabled? | |
| 106 | + * Is malicious bot enabled? | |
| 107 | 107 | * |
| 108 | 108 | * @var bool |
| 109 | 109 | * @defender_property |
| 110 | 110 | */ |
| 111 | - public $bot_trap_enabled = false; | |
| 111 | + public bool $malicious_bot_enabled = false; | |
| 112 | 112 | |
| 113 | 113 | /** |
| 114 | 114 | * How the lock is going to be, if we choose permanent, then their IP will be blacklisted. |
| 115 | 115 | * |
| @@ -116,9 +116,9 @@ | ||
| 116 | 116 | * @var string |
| 117 | 117 | * @defender_property |
| 118 | 118 | * @rule in[temporary,permanent] |
| 119 | 119 | */ |
| 120 | - public $bot_trap_lockout_type = 'temporary'; | |
| 120 | + public $malicious_bot_lockout_type = 'temporary'; | |
| 121 | 121 | |
| 122 | 122 | /** |
| 123 | 123 | * Duration for the lockout. |
| 124 | 124 | * |
| @@ -125,9 +125,9 @@ | ||
| 125 | 125 | * @var int |
| 126 | 126 | * @defender_property |
| 127 | 127 | * @rule required|integer |
| 128 | 128 | */ |
| 129 | - public $bot_trap_lockout_duration = 300; | |
| 129 | + public $malicious_bot_lockout_duration = 300; | |
| 130 | 130 | |
| 131 | 131 | /** |
| 132 | 132 | * Duration unit. |
| 133 | 133 | * |
| @@ -134,28 +134,121 @@ | ||
| 134 | 134 | * @var string |
| 135 | 135 | * @defender_property |
| 136 | 136 | * @rule in[seconds,minutes,hours] |
| 137 | 137 | */ |
| 138 | - public $bot_trap_lockout_duration_unit = 'seconds'; | |
| 138 | + public $malicious_bot_lockout_duration_unit = 'seconds'; | |
| 139 | 139 | |
| 140 | 140 | /** |
| 141 | - * The message to show on frontend when a bot trap is triggered. | |
| 141 | + * The message to show on frontend when a malicious bot is triggered. | |
| 142 | 142 | * |
| 143 | 143 | * @var string |
| 144 | 144 | * @defender_property |
| 145 | 145 | * @sanitize sanitize_textarea_field |
| 146 | 146 | */ |
| 147 | - public $bot_trap_message = ''; | |
| 147 | + public $malicious_bot_message = ''; | |
| 148 | 148 | |
| 149 | 149 | /** |
| 150 | + * Is fake bots enabled? | |
| 151 | + * | |
| 152 | + * @var bool | |
| 153 | + * @defender_property | |
| 154 | + */ | |
| 155 | + public bool $fake_bots_enabled = false; | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * How the lock is going to be, if we choose permanent, then their IP will be blacklisted. | |
| 159 | + * | |
| 160 | + * @var string | |
| 161 | + * @defender_property | |
| 162 | + * @rule in[temporary,permanent] | |
| 163 | + */ | |
| 164 | + public $fake_bots_lockout_type = 'temporary'; | |
| 165 | + | |
| 166 | + /** | |
| 167 | + * Duration for the lockout. | |
| 168 | + * | |
| 169 | + * @var int | |
| 170 | + * @defender_property | |
| 171 | + * @rule required|integer | |
| 172 | + */ | |
| 173 | + public $fake_bots_lockout_duration = 300; | |
| 174 | + | |
| 175 | + /** | |
| 176 | + * Duration unit. | |
| 177 | + * | |
| 178 | + * @var string | |
| 179 | + * @defender_property | |
| 180 | + * @rule in[seconds,minutes,hours] | |
| 181 | + */ | |
| 182 | + public $fake_bots_lockout_duration_unit = 'seconds'; | |
| 183 | + | |
| 184 | + /** | |
| 185 | + * The message to show on frontend when a fake bot is triggered. | |
| 186 | + * | |
| 187 | + * @var string | |
| 188 | + * @defender_property | |
| 189 | + * @sanitize sanitize_textarea_field | |
| 190 | + */ | |
| 191 | + public $fake_bots_message = ''; | |
| 192 | + | |
| 193 | + /** | |
| 194 | + * Old properties of the Bot Trap option for backward compatibility. We can remove them in future versions. | |
| 195 | + * Is bot trap enabled? | |
| 196 | + * | |
| 197 | + * @var bool | |
| 198 | + * @defender_property | |
| 199 | + */ | |
| 200 | + public bool $bot_trap_enabled = false; | |
| 201 | + | |
| 202 | + /** | |
| 203 | + * How the lock is going to be, if we choose permanent, then their IP will be blacklisted. | |
| 204 | + * | |
| 205 | + * @var string | |
| 206 | + * @defender_property | |
| 207 | + * @rule in[temporary,permanent] | |
| 208 | + */ | |
| 209 | + public $bot_trap_lockout_type = 'temporary'; | |
| 210 | + | |
| 211 | + /** | |
| 212 | + * Duration for the lockout. | |
| 213 | + * | |
| 214 | + * @var int | |
| 215 | + * @defender_property | |
| 216 | + * @rule required|integer | |
| 217 | + */ | |
| 218 | + public $bot_trap_lockout_duration = 300; | |
| 219 | + | |
| 220 | + /** | |
| 221 | + * Duration unit. | |
| 222 | + * | |
| 223 | + * @var string | |
| 224 | + * @defender_property | |
| 225 | + * @rule in[seconds,minutes,hours] | |
| 226 | + */ | |
| 227 | + public $bot_trap_lockout_duration_unit = 'seconds'; | |
| 228 | + | |
| 229 | + /** | |
| 150 | 230 | * Rules for validation. |
| 151 | 231 | * |
| 152 | 232 | * @var array |
| 153 | 233 | */ |
| 154 | 234 | protected $rules = array( |
| 155 | - array( array( 'enabled', 'empty_headers', 'blocklist_presets', 'script_presets' ), 'boolean' ), | |
| 156 | - array( array( 'bot_trap_lockout_type' ), 'in', self::BOT_TRAP_LOCKOUT_TYPE_ALLOWED ), | |
| 157 | - array( array( 'bot_trap_lockout_duration_unit' ), 'in', self::BOT_TRAP_LOCKOUT_DURATION_UNIT_ALLOWED ), | |
| 235 | + array( | |
| 236 | + array( | |
| 237 | + 'enabled', | |
| 238 | + 'empty_headers', | |
| 239 | + 'blocklist_presets', | |
| 240 | + 'script_presets', | |
| 241 | + 'malicious_bot_enabled', | |
| 242 | + 'bot_trap_enabled', | |
| 243 | + 'fake_bots_enabled', | |
| 244 | + ), | |
| 245 | + 'boolean', | |
| 246 | + ), | |
| 247 | + array( array( 'malicious_bot_lockout_type' ), 'in', self::BOT_LOCKOUT_TYPE_ALLOWED ), | |
| 248 | + array( array( 'malicious_bot_lockout_duration_unit' ), 'in', self::BOT_LOCKOUT_DURATION_UNIT_ALLOWED ), | |
| 249 | + array( array( 'fake_bots_lockout_type' ), 'in', self::BOT_LOCKOUT_TYPE_ALLOWED ), | |
| 250 | + array( array( 'fake_bots_lockout_duration_unit' ), 'in', self::BOT_LOCKOUT_DURATION_UNIT_ALLOWED ), | |
| 158 | 251 | ); |
| 159 | 252 | |
| 160 | 253 | /** |
| 161 | 254 | * Get default values. |
| @@ -172,9 +265,10 @@ | ||
| 172 | 265 | $message = esc_html__( 'You have been blocked from accessing this website.', 'defender-security' ); |
| 173 | 266 | |
| 174 | 267 | return array( |
| 175 | 268 | 'message' => $message, |
| 176 | - 'bot_trap_message' => $message, | |
| 269 | + 'malicious_bot_message' => $message, | |
| 270 | + 'fake_bots_message' => $message, | |
| 177 | 271 | 'whitelist' => $whitelist, |
| 178 | 272 | // Blocked User Agents. |
| 179 | 273 | 'blacklist' => '', |
| 180 | 274 | 'blocklist_presets' => true, |
| @@ -199,9 +293,10 @@ | ||
| 199 | 293 | $this->blocklist_presets = $default_values['blocklist_presets']; |
| 200 | 294 | $this->blocklist_preset_values = $default_values['blocklist_preset_values']; |
| 201 | 295 | $this->script_presets = $default_values['script_presets']; |
| 202 | 296 | $this->script_preset_values = $default_values['script_preset_values']; |
| 203 | - $this->bot_trap_message = $default_values['bot_trap_message']; | |
| 297 | + $this->malicious_bot_message = $default_values['malicious_bot_message']; | |
| 298 | + $this->fake_bots_message = $default_values['fake_bots_message']; | |
| 204 | 299 | } |
| 205 | 300 | |
| 206 | 301 | /** |
| 207 | 302 | * Checks if the User Agent Lockout feature is active. |
| @@ -208,31 +303,49 @@ | ||
| 208 | 303 | * |
| 209 | 304 | * @return bool Returns true if the User Agent Lockout feature is active, false otherwise. |
| 210 | 305 | */ |
| 211 | 306 | public function is_active(): bool { |
| 212 | - return (bool) apply_filters( | |
| 307 | + $enable = apply_filters( | |
| 213 | 308 | 'wd_user_agents_enable', |
| 214 | 309 | $this->enabled |
| 215 | 310 | ); |
| 311 | + | |
| 312 | + return is_bool( $enable ) ? $enable : (bool) $enable; | |
| 216 | 313 | } |
| 217 | 314 | |
| 218 | 315 | /** |
| 219 | - * Get list of blocklisted or allowlisted data. | |
| 316 | + * Get a list of blocklisted or allowlisted data. | |
| 220 | 317 | * |
| 221 | - * @param string $type blocklist|allowlist. | |
| 318 | + * @param string $type blocklist|allowlist. | |
| 222 | 319 | * @param bool $lower Whether to convert the list to lowercase. |
| 223 | 320 | * |
| 224 | 321 | * @return array |
| 225 | 322 | */ |
| 226 | 323 | public function get_lockout_list( $type = 'blocklist', $lower = true ): array { |
| 227 | - $data = ( 'blocklist' === $type ) ? $this->blacklist : $this->whitelist; | |
| 228 | - $arr = is_array( $data ) ? $data : array_filter( preg_split( "/\r\n|\n|\r/", $data ) ); | |
| 229 | - $arr = array_map( 'trim', $arr ); | |
| 324 | + $data = 'blocklist' === $type ? $this->blacklist : $this->whitelist; | |
| 325 | + $data = trim( $data ); | |
| 326 | + | |
| 327 | + if ( '' === $data ) { | |
| 328 | + return array(); | |
| 329 | + } | |
| 330 | + | |
| 331 | + $arr = preg_split( "/\r\n|\n|\r/", $data ); | |
| 332 | + if ( ! is_array( $arr ) ) { | |
| 333 | + return array(); | |
| 334 | + } | |
| 335 | + | |
| 230 | 336 | if ( $lower ) { |
| 231 | - $arr = array_map( 'strtolower', $arr ); | |
| 337 | + $arr = array_map( | |
| 338 | + function ( $value ) { | |
| 339 | + return strtolower( trim( $value ) ); | |
| 340 | + }, | |
| 341 | + $arr | |
| 342 | + ); | |
| 343 | + } else { | |
| 344 | + $arr = array_map( 'trim', $arr ); | |
| 232 | 345 | } |
| 233 | 346 | |
| 234 | - return $arr; | |
| 347 | + return array_unique( $arr ); | |
| 235 | 348 | } |
| 236 | 349 | |
| 237 | 350 | /** |
| 238 | 351 | * Define settings labels. |
| @@ -256,10 +369,10 @@ | ||
| 256 | 369 | * |
| 257 | 370 | * @return array |
| 258 | 371 | */ |
| 259 | 372 | public function get_access_status( $ua ): array { |
| 260 | - $blocklist = str_replace( '#', '\#', $this->get_all_selected_blocklist_ua() ); | |
| 261 | - $allowlist = str_replace( '#', '\#', $this->get_lockout_list( 'allowlist' ) ); | |
| 373 | + $blocklist = $this->get_all_selected_blocklist_ua(); | |
| 374 | + $allowlist = $this->get_lockout_list( 'allowlist' ); | |
| 262 | 375 | |
| 263 | 376 | // Escape regex special characters in each item before building the pattern. |
| 264 | 377 | $blocklist_escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '#' ) ); |
| 265 | 378 | $allowlist_escaped = array_map( 'preg_quote', $allowlist, array_fill( 0, count( $allowlist ), '#' ) ); |
| @@ -266,12 +379,12 @@ | ||
| 266 | 379 | |
| 267 | 380 | $blocklist_regex_pattern = '#' . implode( '|', $blocklist_escaped ) . '#i'; |
| 268 | 381 | $allowlist_regex_pattern = '#' . implode( '|', $allowlist_escaped ) . '#i'; |
| 269 | 382 | |
| 270 | - $blocklist_match = preg_match( $blocklist_regex_pattern, $ua ); | |
| 271 | - $allowlist_match = preg_match( $allowlist_regex_pattern, $ua ); | |
| 383 | + $blocklist_match = count( $blocklist ) > 0 && 1 === preg_match( $blocklist_regex_pattern, $ua ); | |
| 384 | + $allowlist_match = count( $allowlist ) > 0 && 1 === preg_match( $allowlist_regex_pattern, $ua ); | |
| 272 | 385 | |
| 273 | - if ( empty( $blocklist_match ) && empty( $allowlist_match ) ) { | |
| 386 | + if ( ! $blocklist_match && ! $allowlist_match ) { | |
| 274 | 387 | return array( 'na' ); |
| 275 | 388 | } |
| 276 | 389 | |
| 277 | 390 | $result = array(); |
| @@ -276,14 +389,14 @@ | ||
| 276 | 389 | |
| 277 | 390 | $result = array(); |
| 278 | 391 | |
| 279 | 392 | // Check blocklist first - if it matches, add 'banned'. |
| 280 | - if ( ! empty( $blocklist_match ) ) { | |
| 393 | + if ( $blocklist_match ) { | |
| 281 | 394 | $result[] = 'banned'; |
| 282 | 395 | } |
| 283 | 396 | |
| 284 | 397 | // Check allowlist - if it matches, add 'allowlist'. |
| 285 | - if ( ! empty( $allowlist_match ) ) { | |
| 398 | + if ( $allowlist_match ) { | |
| 286 | 399 | $result[] = 'allowlist'; |
| 287 | 400 | } |
| 288 | 401 | |
| 289 | 402 | return $result; |
| @@ -289,102 +402,8 @@ | ||
| 289 | 402 | return $result; |
| 290 | 403 | } |
| 291 | 404 | |
| 292 | 405 | /** |
| 293 | - * Checks if a user agent is present in a given list. | |
| 294 | - * | |
| 295 | - * @param string $ua The user agent to check. | |
| 296 | - * @param string $collection The type of list to check against ('blocklist' or 'allowlist'). | |
| 297 | - * | |
| 298 | - * @return bool Returns true if the user agent is in the list, false otherwise. | |
| 299 | - */ | |
| 300 | - public function is_ua_in_list( $ua, $collection ): bool { | |
| 301 | - if ( 'allowlist' === $collection ) { | |
| 302 | - $arr = str_replace( '#', '\#', $this->get_lockout_list( $collection ) ); | |
| 303 | - } else { | |
| 304 | - $arr = str_replace( '#', '\#', $this->get_all_selected_blocklist_ua() ); | |
| 305 | - } | |
| 306 | - | |
| 307 | - // Escape regex special characters in each item before building the pattern. | |
| 308 | - $arr_escaped = array_map( 'preg_quote', $arr, array_fill( 0, count( $arr ), '#' ) ); | |
| 309 | - $list_regex_pattern = '#' . implode( '|', $arr_escaped ) . '#i'; | |
| 310 | - | |
| 311 | - $list_match = preg_match( $list_regex_pattern, $ua ); | |
| 312 | - | |
| 313 | - return ! empty( $list_match ); | |
| 314 | - } | |
| 315 | - | |
| 316 | - /** | |
| 317 | - * Remove User Agent from a list. | |
| 318 | - * | |
| 319 | - * @param string $ua The user agent to remove. | |
| 320 | - * @param string $collection blocklist|allowlist. | |
| 321 | - * | |
| 322 | - * @return void | |
| 323 | - */ | |
| 324 | - public function remove_from_list( $ua, $collection ) { | |
| 325 | - if ( 'allowlist' === $collection ) { | |
| 326 | - $arr = $this->get_lockout_list( $collection ); | |
| 327 | - // Array can contain uppercase. | |
| 328 | - $orig_arr = str_replace( '#', '\#', $this->get_lockout_list( $collection, false ) ); | |
| 329 | - } else { | |
| 330 | - $arr = $this->get_all_selected_blocklist_ua(); | |
| 331 | - // Array can contain uppercase. | |
| 332 | - $orig_arr = str_replace( '#', '\#', $this->get_all_selected_blocklist_ua( false ) ); | |
| 333 | - } | |
| 334 | - | |
| 335 | - $list_regex_pattern = '#' . implode( '|', $arr ) . '#i'; | |
| 336 | - $list_match = preg_match( $list_regex_pattern, $ua ); | |
| 337 | - if ( false !== $list_match ) { | |
| 338 | - if ( 'blocklist' === $collection ) { | |
| 339 | - // Check in 'Blocklist Presets'. | |
| 340 | - if ( $this->blocklist_presets ) { | |
| 341 | - $key = array_search( $ua, $this->blocklist_preset_values, true ); | |
| 342 | - if ( false !== $key ) { | |
| 343 | - unset( $this->blocklist_preset_values[ $key ] ); | |
| 344 | - $this->blocklist_preset_values = array_values( $this->blocklist_preset_values ); | |
| 345 | - } | |
| 346 | - } | |
| 347 | - // Check in 'Script Presets' using Regex. | |
| 348 | - if ( $this->script_presets ) { | |
| 349 | - if ( false !== strpos( $ua, User_Agent_Service::GO_HTTP_CLIENT_KEY . '/' ) ) { | |
| 350 | - $key_script_preset = User_Agent_Service::GO_HTTP_CLIENT_KEY; | |
| 351 | - } elseif ( false !== strpos( $ua, User_Agent_Service::PYTHON_REQUESTS_KEY . '/' ) ) { | |
| 352 | - $key_script_preset = User_Agent_Service::PYTHON_REQUESTS_KEY; | |
| 353 | - } else { | |
| 354 | - $key_script_preset = ''; | |
| 355 | - } | |
| 356 | - | |
| 357 | - $key = array_search( $key_script_preset, $this->script_preset_values, true ); | |
| 358 | - if ( false !== $key ) { | |
| 359 | - unset( $this->script_preset_values[ $key ] ); | |
| 360 | - $this->script_preset_values = array_values( $this->script_preset_values ); | |
| 361 | - } | |
| 362 | - } | |
| 363 | - // Check 'Custom User Agents' case. | |
| 364 | - $arr_blocklist = $this->get_lockout_list( 'blocklist', false ); | |
| 365 | - if ( ! empty( $arr_blocklist ) ) { | |
| 366 | - $key = array_search( $ua, $arr_blocklist, true ); | |
| 367 | - if ( false !== $key && isset( $arr_blocklist[ $key ] ) ) { | |
| 368 | - unset( $arr_blocklist[ $key ] ); | |
| 369 | - // Convert back to string. | |
| 370 | - $this->blacklist = implode( PHP_EOL, $arr_blocklist ); | |
| 371 | - } | |
| 372 | - } | |
| 373 | - } elseif ( 'allowlist' === $collection ) { | |
| 374 | - $key = array_search( $ua, $arr, true ); | |
| 375 | - if ( false !== $key && isset( $arr[ $key ] ) ) { | |
| 376 | - unset( $arr[ $key ] ); | |
| 377 | - // Convert back to string. | |
| 378 | - $this->whitelist = implode( PHP_EOL, $arr ); | |
| 379 | - } | |
| 380 | - } | |
| 381 | - | |
| 382 | - $this->save(); | |
| 383 | - } | |
| 384 | - } | |
| 385 | - | |
| 386 | - /** | |
| 387 | 406 | * Add an UA to the list. |
| 388 | 407 | * |
| 389 | 408 | * @param string $ua User agent name. |
| 390 | 409 | * @param string $collection blocklist|allowlist. |
| @@ -425,8 +444,23 @@ | ||
| 425 | 444 | return implode( PHP_EOL, $arr ); |
| 426 | 445 | } |
| 427 | 446 | |
| 428 | 447 | /** |
| 448 | + * Filter the UAs, as we use a textarea to submit, so it can contain some un-valid UAs. | |
| 449 | + */ | |
| 450 | + protected function after_validate(): void { | |
| 451 | + $this->blacklist = implode( | |
| 452 | + PHP_EOL, | |
| 453 | + array_filter( $this->get_lockout_list( 'blocklist' ), 'strlen' ) | |
| 454 | + ); | |
| 455 | + | |
| 456 | + $this->whitelist = implode( | |
| 457 | + PHP_EOL, | |
| 458 | + array_filter( $this->get_lockout_list( 'allowlist' ), 'strlen' ) | |
| 459 | + ); | |
| 460 | + } | |
| 461 | + | |
| 462 | + /** | |
| 429 | 463 | * Get the module name. |
| 430 | 464 | * |
| 431 | 465 | * @return string |
| 432 | 466 | */ |
| @@ -457,6 +491,15 @@ | ||
| 457 | 491 | $blocklist_presets = $this->blocklist_presets ? $this->blocklist_preset_values : array(); |
| 458 | 492 | $script_presets = $this->script_presets ? $this->script_preset_values : array(); |
| 459 | 493 | |
| 460 | 494 | return array_merge( $blocklist_custom, $blocklist_presets, $script_presets ); |
| 495 | + } | |
| 496 | + | |
| 497 | + /** | |
| 498 | + * Return the module slug. | |
| 499 | + * | |
| 500 | + * @return string | |
| 501 | + */ | |
| 502 | + public static function get_module_slug(): string { | |
| 503 | + return 'ua-lockout'; | |
| 461 | 504 | } |
| 462 | 505 | } |