PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 3.1.1 All 140 releases
defender-security / src / component / class-cli.php

class-cli.php in Defender Security – Malware Scanner, Login Security & Firewall trunk, at src/component/class-cli.php

1,524 lines 43.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This file contains the Cli class which is used to handle WP-CLI commands for the WP Defender plugin.
4 * It provides methods to manage scans, audits, firewall settings, and more through the command line.
5 *
6 * @package WP_Defender\Component
7 */
8
9 namespace WP_Defender\Component;
10
11 use WP_CLI;
12 use Countable;
13 use Exception;
14 use Throwable;
15 use Faker\Factory;
16 use WP_Filesystem_Base;
17 use WP_CLI\ExitException;
18 use WP_Defender\Traits\IO;
19 use WP_Defender\Traits\Theme;
20 use WP_Defender\Traits\Plugin;
21 use WP_Defender\Traits\Formats;
22 use WP_Defender\Behavior\WPMUDEV;
23 use WP_Defender\Component\Audit;
24 use WP_Defender\Model\Audit_Log;
25 use WP_Defender\Model\Scan_Item;
26 use WP_Defender\Model\Lockout_Ip;
27 use WP_Defender\Model\Lockout_Log;
28 use WP_Defender\Controller\Dashboard;
29 use WP_Defender\Controller\Two_Factor;
30 use WP_Defender\Controller\Login_Access;
31 use WP_Defender\Controller\Main_Setting;
32 use WP_Defender\Controller\Audit_Logging;
33 use WP_Defender\Model\Scan as Model_Scan;
34 use WP_Defender\Controller\Security_Tweaks;
35 use WP_Defender\Model\Setting\Login_Lockout;
36 use WP_Defender\Behavior\Scan\Core_Integrity;
37 use WP_Defender\Controller\Blocklist_Monitor;
38 use WP_Defender\Model\Setting\Password_Reset;
39 use WP_Defender\Model\Setting\Notfound_Lockout;
40 use WP_Defender\Model\Setting\Security_Headers;
41 use WP_Defender\Component\Scan as Scan_Component;
42 use WP_Defender\Component\Logger\Rotation_Logger;
43 use WP_Defender\Model\Setting\User_Agent_Lockout;
44 use function WP_CLI\Utils\format_items;
45
46 if ( ! defined( 'WPINC' ) ) {
47 die;
48 }
49
50 /**
51 * Class Cli
52 */
53 class Cli {
54
55 use Formats {
56 calculate_date_interval as protected;
57 format_bytes_into_readable as protected;
58 format_date_time as protected;
59 get_date as protected;
60 get_days_of_week as protected;
61 get_times as protected;
62 get_timezone_string as protected;
63 local_to_utc as protected;
64 moment_datetime_format_from as protected;
65 persistent_hub_datetime_format as protected;
66 time_since as protected;
67 get_local_human_date as protected;
68 get_time_diff as protected;
69 }
70 use IO {
71 try_create_lock as protected;
72 release_cron_lock as protected;
73 remove_lock as protected;
74 acquire_cron_lock as protected;
75 compare_hashes as protected;
76 delete_dir as protected;
77 detect_line_ending as protected;
78 get_log_path as protected;
79 }
80 use Theme {
81 get_path_of_themes_dir as protected;
82 get_theme as protected;
83 get_theme_slugs as protected;
84 get_themes as protected;
85 is_active_theme as protected;
86 }
87 use Plugin {
88 check_plugin_on_wp_org as protected;
89 check_by_readme_file as protected;
90 get_abs_plugin_path_by_slug as protected;
91 get_active_plugin_names as protected;
92 get_plugin_details_by as protected;
93 get_plugin_directory_name as protected;
94 get_plugin_headers as protected;
95 get_plugin_relative_path as protected;
96 get_plugin_slugs as protected;
97 get_plugins as protected;
98 get_plugin_slug_by as protected;
99 handle_wp_org_response_by as protected;
100 is_active_plugin as protected;
101 is_likely_wporg_slug as protected;
102 ping_wp_org_by_plugin_slug as protected;
103 }
104
105 /**
106 * Run scans and manage scan results via WP-CLI.
107 *
108 * ## OPTIONS
109 *
110 * <command>
111 * : Action to perform.
112 * ---
113 * options:
114 * - run
115 * - ignore
116 * - unignore
117 * - resolve
118 * - delete
119 * - clear_logs
120 * ---
121 *
122 * [--type=<type>]
123 * : Filter by issue type. Omit to target all types.
124 * ---
125 * options:
126 * - detailed
127 * - core_integrity
128 * - plugin_integrity
129 * - vulnerability
130 * - suspicious_code
131 * - plugin_outdated
132 * - plugin_closed
133 * ---
134 *
135 * ## EXAMPLES
136 *
137 * # Run a full scan.
138 * $ wp defender scan run
139 * Success: All done!
140 *
141 * # Run a detailed scan with table output.
142 * $ wp defender scan run --type=detailed
143 *
144 * # Ignore all active core integrity issues.
145 * $ wp defender scan ignore --type=core_integrity
146 *
147 * # Resolve all active vulnerability issues.
148 * $ wp defender scan resolve --type=vulnerability
149 *
150 * # Delete all suspicious code files.
151 * $ wp defender scan delete --type=suspicious_code
152 *
153 * # Clear completed scan logs.
154 * $ wp defender scan clear_logs
155 *
156 * @param mixed $args Command arguments.
157 * @param mixed $options Command options.
158 *
159 * @throws ExitException If an invalid command is provided.
160 */
161 public function scan( $args, $options ) {
162 if ( ! is_array( $args ) || array() === $args ) {
163 WP_CLI::error( 'Invalid command' );
164
165 return;
166 }
167 [$command] = $args;
168 switch ( $command ) {
169 case 'run':
170 $this->scan_all( $options );
171 break;
172 case 'clear_logs':
173 $this->scan_clear_logs();
174 break;
175 default:
176 $commands = array( 'ignore', 'unignore', 'resolve', 'delete' );
177 if ( in_array( $command, $commands, true ) ) {
178 WP_CLI::confirm( 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?', $options );
179 $this->scan_task( $command, $options );
180 } else {
181 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
182 }
183 break;
184 }
185 }
186
187 /**
188 * Starts a full scan based on the provided options.
189 *
190 * @param array $options Command options.
191 */
192 private function scan_all( $options ) {
193 $type = $options['type'] ?? null;
194 $is_detailed = false;
195 switch ( $type ) {
196 case null:
197 // All items.
198 $type = null;
199 break;
200 case 'detailed':
201 $is_detailed = true;
202 break;
203 default:
204 WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
205 break;
206 }
207 $scan_component = wd_di()->get( Scan_Component::class );
208 if ( ! $scan_component->is_any_scan_type_active() ) {
209 WP_CLI::error( Scan_Component::get_emergency_scan_stop_text() );
210 }
211 WP_CLI::log( 'Check if there is a scan ongoing...' );
212 $scan = Model_Scan::get_active();
213 if ( ! is_object( $scan ) ) {
214 WP_CLI::log( 'No active scan, creating...' );
215 // Match the web-triggered flow: clear stale idle scans first so they don't skew "last scan" lookups.
216 wd_di()->get( Model_Scan::class )->delete_idle();
217 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
218 $scan = Model_Scan::create();
219 if ( is_wp_error( $scan ) ) {
220 WP_CLI::error( $scan->get_error_message() );
221 }
222 $scan_component->gather_actioned_plugin_details();
223 } else {
224 WP_CLI::log( 'Continue from last scan' );
225 }
226 // Start detailed scan.
227 if ( $is_detailed ) {
228 $start = microtime( true );
229 }
230 $handler = wd_di()->get( Scan_Component::class );
231 while ( $handler->process() === false ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedWhile
232 }
233 $scan = Model_Scan::get_last();
234 if ( ! is_object( $scan ) || is_wp_error( $scan ) ) {
235 return;
236 }
237 $results = $scan->to_array();
238 if ( is_array( $results ) && isset( $results['issues_items'] ) && array() !== $results['issues_items'] ) {
239 $count = is_array( $results['issues_items'] ) || $results['issues_items'] instanceof Countable ? count( $results['issues_items'] ) : 0;
240 // Finish detailed scan.
241 if ( $is_detailed ) {
242 format_items( 'table', $results['issues_items'], array( 'type', 'short_desc', 'full_path' ) );
243 WP_CLI::log( sprintf( 'Saved %d items.', $count ) );
244 $finish = microtime( true ) - $start;
245 WP_CLI::log( 'Scan takes ' . round( $finish, 2 ) . 's to process.' );
246 } else {
247 WP_CLI::log( sprintf( 'Found %d issues.', $count ) );
248 }
249 }
250 WP_CLI::success( 'All done!' );
251 }
252
253 /**
254 * Clear completed action scheduler logs.
255 */
256 private function scan_clear_logs() {
257 $scan_component = wd_di()->get( Scan_Component::class );
258 $result = $scan_component::clear_logs();
259 $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
260
261 WP_CLI::log( $message );
262 }
263
264 /**
265 * Executes tasks based on the type of scan.
266 *
267 * @param string $command The task to perform.
268 * @param mixed $options Command options.
269 */
270 private function scan_task( $command, $options ) {
271 $option_type = is_array( $options ) ? ( $options['type'] ?? null ) : null;
272 $type = is_string( $option_type ) && '' !== $option_type ? strtolower( $option_type ) : null;
273 if ( defender_is_wp_org_version() && in_array(
274 $type,
275 array(
276 Scan_Item::TYPE_VULNERABILITY, // TYPE_SUSPICIOUS const is not suitable for use.
277 'suspicious_code',
278
279 ),
280 true
281 ) ) {
282 WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
283 return;
284 }
285
286 switch ( $type ) {
287 case null:
288 // All items.
289 $type = null;
290 break;
291 case 'core_integrity':
292 $type = Scan_Item::TYPE_INTEGRITY;
293 break;
294 case 'plugin_integrity':
295 $type = Scan_Item::TYPE_PLUGIN_CHECK;
296 break;
297 case 'vulnerability':
298 $type = Scan_Item::TYPE_VULNERABILITY;
299 break;
300 case 'suspicious_code':
301 $type = Scan_Item::TYPE_SUSPICIOUS;
302 break;
303 case 'plugin_outdated':
304 $type = Scan_Item::TYPE_PLUGIN_OUTDATED;
305 break;
306 case 'plugin_closed':
307 $type = Scan_Item::TYPE_PLUGIN_CLOSED;
308 break;
309 default:
310 WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
311 break;
312 }
313 $active = Model_Scan::get_active();
314 if ( is_object( $active ) ) {
315 WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
316 }
317 $model = Model_Scan::get_last();
318 if ( ! is_object( $model ) ) {
319 return;
320 }
321 switch ( $command ) {
322 case 'ignore':
323 $issues = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
324 foreach ( $issues as $issue ) {
325 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $issue->raw_data );
326 if ( $model->ignore_issue( $issue->id ) ) {
327 WP_CLI::log( sprintf( 'Ignoring %s: %s', $issue_data['type'], $issue_data['path'] ) );
328 }
329 }
330 WP_CLI::log( sprintf( 'Ignored %s items', count( $issues ) ) );
331 break;
332 case 'unignore':
333 $issues = $model->get_issues( $type, Scan_Item::STATUS_IGNORE );
334 foreach ( $issues as $issue ) {
335 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $issue->raw_data );
336 if ( $model->unignore_issue( $issue->id ) ) {
337 WP_CLI::log( sprintf( 'Unignoring %s: %s', $issue_data['type'], $issue_data['path'] ) );
338 }
339 }
340 WP_CLI::log( sprintf( 'Unignored %s items', count( $issues ) ) );
341 break;
342 case 'resolve':
343 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
344 $resolved = array();
345 foreach ( $items as $item ) {
346 if ( in_array( $item->type, array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ), true ) ) {
347 WP_CLI::log( sprintf( 'Reverting %s to original', $item->raw_data['file'] ) );
348 $ret = $item->resolve();
349 if ( ! is_wp_error( $ret ) ) {
350 $resolved[] = $item;
351 } else {
352 WP_CLI::error( $ret->get_error_message() );
353 }
354 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $item->type ) {
355 // If this is content, we will try to delete them.
356 $whitelist = array(// wordfence waf.
357 ABSPATH . '/wordfence-waf.php', // Any files inside plugins, if removed, can cause fatal error.
358 WP_CONTENT_DIR . '/plugins/', // Any files inside themes.
359 $this->get_path_of_themes_dir(),
360 );
361 $path = $item->raw_data['file'];
362 $can_delete = true;
363 foreach ( $whitelist as $value ) {
364 $current = $value;
365 if ( str_contains( $path, $value ) ) {
366 // Ignore this.
367 $can_delete = false;
368 break;
369 }
370 }
371 if ( false === $can_delete ) {
372 WP_CLI::log( sprintf( 'Ignore file %s as it is in %s', $path, $current ) );
373 } elseif ( ! is_dir( $path ) && wp_delete_file( $path ) ) {
374 WP_CLI::log( sprintf( 'Delete file %s', $path ) );
375 $model->remove_issue( $item->id );
376 $resolved[] = $item;
377 } else {
378 WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
379 }
380 } elseif ( Scan_Item::TYPE_VULNERABILITY === $item->type ) {
381 $ret = $item->resolve();
382 if ( is_wp_error( $ret ) ) {
383 WP_CLI::error( $ret->get_error_message() );
384 } elseif ( is_array( $ret ) && isset( $ret['type_notice'] ) && 'error' === $ret['type_notice'] ) {
385 WP_CLI::error( $ret['message'] ?? esc_html__( 'Unable to resolve vulnerability.', 'defender-security' ) );
386 } else {
387 $model->remove_issue( $item->id );
388 $resolved[] = $item;
389 }
390 }
391 }
392 WP_CLI::log( sprintf( 'Resolved %s items', count( $resolved ) ) );
393 break;
394 case 'delete':
395 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
396 $deleted = array();
397 foreach ( $items as $item ) {
398 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $item->raw_data );
399 $path = $issue_data['path'];
400 $issue_type = $issue_data['type'];
401 if ( ! file_exists( $path ) ) {
402 continue;
403 }
404 // Work with plugin dir or single file, e.g. for Vulnerability, Outdated or Closed plugin types.
405 if ( 'folder' === $issue_type ) {
406 if ( $this->is_active_plugin( $path ) ) {
407 WP_CLI::warning( sprintf( 'This plugin %s cannot be removed because it is active.', $path ) );
408 continue;
409 }
410
411 if ( is_dir( $path ) ) {
412 if ( $this->delete_dir( $path ) ) {
413 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
414 $model->remove_issue( $item->id );
415 $deleted[] = $item;
416 }
417 } elseif ( wp_delete_file( $path ) ) {
418 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
419 $model->remove_issue( $item->id );
420 $deleted[] = $item;
421 } else {
422
423 WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
424 }
425 } elseif ( 'file' === $issue_type ) {
426 // Work with core_integrity, plugin_integrity or suspicious_code types.
427 if ( wp_delete_file( $path ) ) {
428 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
429 $model->remove_issue( $item->id );
430 $deleted[] = $item;
431 } else {
432 WP_CLI::warning( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
433 }
434 }
435 }
436 WP_CLI::log( sprintf( 'Deleted %s items', count( $deleted ) ) );
437 break;
438 default:
439 break;
440 }
441 }
442
443 /**
444 * Split scan issue into file and dir.
445 *
446 * @param string|null $type Scan type.
447 * @param array $raw_data Array of raw scan data.
448 *
449 * @return array
450 */
451 private function split_scan_issue_into_file_and_dir( $type, $raw_data ): array {
452 // General case without type-param.
453 if ( null === $type ) {
454 if ( isset( $raw_data['file'] ) ) {
455 return array(
456 'type' => 'file',
457 'path' => $raw_data['file'],
458 );
459 } elseif ( isset( $raw_data['base_slug'] ) ) {
460 return array(
461 'type' => 'folder',
462 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
463 );
464 } elseif ( isset( $raw_data['slug'] ) ) {
465 return array(
466 'type' => 'folder',
467 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
468 );
469 }
470 }
471
472 if ( in_array( $type, array( Scan_Item::TYPE_PLUGIN_OUTDATED, Scan_Item::TYPE_PLUGIN_CLOSED ), true ) ) {
473 return array(
474 'type' => 'folder',
475 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
476 );
477 } elseif ( Scan_Item::TYPE_VULNERABILITY === $type ) {
478 return array(
479 'type' => 'folder',
480 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
481 );
482 } else {
483 return array(
484 'type' => 'file',
485 'path' => $raw_data['file'],
486 );
487 }
488 }
489
490 /**
491 * Generate dummy data, use in unit tests.
492 * DO NOT USE IN PRODUCTION.
493 *
494 * @param mixed $args Command arguments.
495 */
496 public function seed( $args ) {
497 global $wp_filesystem;
498 // Initialize the WP filesystem, no more using 'file-put-contents' function.
499 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
500 require_once ABSPATH . '/wp-admin/includes/file.php';
501 WP_Filesystem();
502 }
503 if ( ! is_array( $args ) || array() === $args ) {
504 WP_CLI::error( 'Invalid command' );
505
506 return;
507 }
508 if ( ! $this->is_testing_mode() ) {
509 return;
510 }
511
512 [ $command ] = $args;
513 switch ( $command ) {
514 case 'scan:core':
515 WP_CLI::confirm( 'This will modify a WordPress core file (wp-load.php). Are you sure?', array() );
516
517 $file_path = ABSPATH . 'wp-load.php';
518 if ( ! $wp_filesystem->exists( $file_path ) ) {
519 WP_CLI::error( sprintf( 'File does not exist: %s', $file_path ) );
520
521 return;
522 }
523 $content = $wp_filesystem->get_contents( $file_path );
524 if ( false === $content ) {
525 WP_CLI::error( sprintf( 'Could not read file: %s', $file_path ) );
526
527 return;
528 }
529 if ( str_contains( $content, '//this make different' ) ) {
530 WP_CLI::warning( 'File already seeded, skipping.' );
531
532 return;
533 }
534 $wp_filesystem->put_contents( $file_path, $content . '//this make different' );
535 break;
536 case 'ip:logs':
537 WP_CLI::confirm( 'This will insert fake firewall lockout log entries into the database. Are you sure?', array() );
538 // We will generate randomly 10k logs in 3 months.
539 $types = array( Lockout_Log::AUTH_FAIL, Lockout_Log::AUTH_LOCK, Lockout_Log::ERROR_404, Lockout_Log::LOCKOUT_404, Lockout_Log::LOCKOUT_UA );
540 $is_lock = array( Lockout_Log::AUTH_LOCK, Lockout_Log::LOCKOUT_404, Lockout_Log::LOCKOUT_UA );
541 $faker = Factory::create();
542 WP_CLI::log( $faker->ipv4 );
543 $range = array(
544 'today midnight' => array( 'now', 100 ),
545 '-6 days' => array( 'yesterday', 50 ),
546 '-30 days' => array( '-7 days', 70 ),
547 );
548 $counter = array(
549 'last_24_hours' => 0,
550 'last_30_days' => 0,
551 'login_lockout' => 0,
552 '404_lockout' => 0,
553 'ua_lockout' => 0,
554 );
555 $last_lockout = 0;
556 foreach ( $range as $date => $to ) {
557 [$to, $count] = $to;
558 for ( $i = 0; $i < $count; $i++ ) {
559 $model = new Lockout_Log();
560 $model->ip = $faker->ipv4;
561 $model->type = $types[ array_rand( $types ) ];
562 $model->log = $faker->sentence( 20 );
563 $model->date = $faker->dateTimeBetween( $date, $to )->getTimestamp();
564 $model->blog_id = 1;
565 $model->tried = $faker->userName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
566 $model->country_iso_code = $faker->countryCode; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
567 $model->save();
568 if ( ( $model->date > $last_lockout ) ) {
569 $last_lockout = $model->date;
570 }
571 if ( in_array( $model->type, $is_lock, true ) ) {
572 $counter['last_30_days'] += 1;
573 if ( $model->date > strtotime( 'yesterday midnight' ) ) {
574 $counter['last_24_hours'] += 1;
575 }
576 if ( $model->date > strtotime( '-6 days', strtotime( 'today midnight' ) ) ) {
577 if ( Lockout_Log::AUTH_LOCK === $model->type ) {
578 $counter['login_lockout'] += 1;
579 } elseif ( Lockout_Log::LOCKOUT_404 === $model->type ) {
580 $counter['404_lockout'] += 1;
581 } else {
582 $counter['ua_lockout'] += 1;
583 }
584 }
585 }
586 }
587 }
588 $counter['last_lockout'] = $this->format_date_time( $last_lockout );
589 echo wp_json_encode( $counter );
590 break;
591 default:
592 WP_CLI::error( 'Invalid command' );
593 break;
594 }
595 }
596
597 /**
598 * Clean up dummy data.
599 * DO NOT USE IN PRODUCTION.
600 *
601 * @param mixed $args Command arguments.
602 */
603 public function unseed( $args ) {
604 global $wp_filesystem;
605 // Initialize the WP filesystem, no more using 'file-put-contents' function.
606 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
607 require_once ABSPATH . '/wp-admin/includes/file.php';
608 WP_Filesystem();
609 }
610 if ( ! is_array( $args ) || array() === $args ) {
611 WP_CLI::error( 'Invalid command' );
612
613 return;
614 }
615 if ( ! $this->is_testing_mode() ) {
616 return;
617 }
618
619 [ $command ] = $args;
620 switch ( $command ) {
621 case 'scan:core':
622 WP_CLI::confirm( 'This will revert the modification to wp-load.php. Are you sure?', array() );
623
624 $file_path = ABSPATH . 'wp-load.php';
625 if ( ! $wp_filesystem->exists( $file_path ) ) {
626 WP_CLI::error( sprintf( 'File does not exist: %s', $file_path ) );
627
628 return;
629 }
630 $content = $wp_filesystem->get_contents( $file_path );
631 if ( false === $content ) {
632 WP_CLI::error( sprintf( 'Could not read file: %s', $file_path ) );
633
634 return;
635 }
636 if ( ! str_contains( $content, '//this make different' ) ) {
637 WP_CLI::warning( 'Marker not found in file, nothing to revert.' );
638
639 return;
640 }
641 $wp_filesystem->put_contents( $file_path, str_replace( '//this make different', '', $content ) );
642 break;
643 case 'scan:suspicious':
644 WP_CLI::confirm( 'This will delete the false-positive test file. Are you sure?', array() );
645 wp_delete_file( WP_CONTENT_DIR . '/false-positive.php' );
646 break;
647 default:
648 break;
649 }
650 }
651
652 /**
653 * Manage audit logs via WP-CLI.
654 *
655 * ## OPTIONS
656 *
657 * <command>
658 * : Action to perform.
659 * ---
660 * options:
661 * - reset
662 * - sync
663 * ---
664 *
665 * ## EXAMPLES
666 *
667 * # Delete all audit log entries from the database.
668 * $ wp defender audit reset
669 * All clear
670 *
671 * # Synchronize local audit logs with cloud history (Pro only).
672 * $ wp defender audit sync
673 * Sync completed.
674 *
675 * @param mixed $args Command arguments.
676 */
677 public function audit( $args ) {
678 if ( ! is_array( $args ) || array() === $args ) {
679 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
680 WP_CLI::runcommand(
681 'defender audit --help',
682 array(
683 'launch' => false,
684 'exit_error' => false,
685 )
686 );
687
688 return;
689 }
690
691 [$command] = $args;
692 switch ( $command ) {
693 case 'reset':
694 wd_di()->get( Audit::class )->reset();
695
696 WP_CLI::log( 'All clear' );
697 break;
698 default:
699 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
700 WP_CLI::runcommand(
701 'defender audit --help',
702 array(
703 'launch' => false,
704 'exit_error' => false,
705 )
706 );
707 break;
708 }
709 }
710
711 /**
712 * Manage security headers via WP-CLI.
713 *
714 * ## OPTIONS
715 *
716 * <command>
717 * : Action to perform.
718 * ---
719 * options:
720 * - check
721 * - activate
722 * - deactivate
723 * ---
724 *
725 * ## EXAMPLES
726 *
727 * # Check the current status of all security headers.
728 * $ wp defender security_headers check
729 * Success: Checking is ready.
730 *
731 * # Activate all security headers.
732 * $ wp defender security_headers activate
733 * Activating is ready.
734 *
735 * # Deactivate all security headers.
736 * $ wp defender security_headers deactivate
737 * Deactivating is ready.
738 *
739 * @param mixed $args Command arguments.
740 *
741 * @throws ExitException|Exception If an invalid command is provided.
742 */
743 public function security_headers( $args ) {
744 if ( ! is_array( $args ) || array() === $args ) {
745 WP_CLI::error( 'Invalid command.' );
746
747 return;
748 }
749 $model = new Security_Headers();
750 [$command] = $args;
751 switch ( $command ) {
752 case 'check':
753 $i = 1;
754 foreach ( $model->get_headers() as $header ) {
755 $state = true === $header->check() ? 'enabled' : 'disabled';
756 WP_CLI::log( sprintf( '#%s - %s is %s', $i, $header->get_title(), $state ) );
757 ++$i;
758 }
759 WP_CLI::success( 'Checking is ready.' );
760 break;
761 case 'activate':
762 foreach ( $model->get_headers() as $rule_slug => $header ) {
763 $this->set_security_header_state( $model, $rule_slug, true );
764 }
765 $model->save();
766 WP_CLI::log( 'Activating is ready.' );
767 break;
768 case 'deactivate':
769 foreach ( $model->get_headers() as $rule_slug => $header ) {
770 $this->set_security_header_state( $model, $rule_slug, false );
771 }
772 $model->save();
773 WP_CLI::log( 'Deactivating is ready.' );
774 break;
775 default:
776 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
777 break;
778 }
779 }
780
781 /**
782 * Set a security header setting without using dynamic model properties.
783 *
784 * @param Security_Headers $model The security headers settings model.
785 * @param string $rule_slug The header rule slug.
786 * @param bool $enabled Whether the rule is enabled.
787 */
788 private function set_security_header_state( Security_Headers $model, string $rule_slug, bool $enabled ): void {
789 switch ( $rule_slug ) {
790 case 'sh_xframe':
791 $model->sh_xframe = $enabled;
792 break;
793 case 'sh_xss_protection':
794 $model->sh_xss_protection = $enabled;
795 break;
796 case 'sh_content_type_options':
797 $model->sh_content_type_options = $enabled;
798 break;
799 case 'sh_strict_transport':
800 $model->sh_strict_transport = $enabled;
801 break;
802 case 'sh_referrer_policy':
803 $model->sh_referrer_policy = $enabled;
804 break;
805 case 'sh_feature_policy':
806 $model->sh_feature_policy = $enabled;
807 break;
808 }
809 }
810
811 /**
812 * Manage plugin settings via WP-CLI.
813 *
814 * ## OPTIONS
815 *
816 * <command>
817 * : Action to perform.
818 * ---
819 * options:
820 * - reset
821 * ---
822 *
823 * ## EXAMPLES
824 *
825 * # Reset all plugin settings to defaults.
826 * $ wp defender settings reset
827 * All cleared!
828 *
829 * @param mixed $args Command arguments.
830 * @param mixed $options Command options.
831 */
832 public function settings( $args, $options ) {
833 if ( ! is_array( $args ) || array() === $args ) {
834 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
835 WP_CLI::runcommand(
836 'defender settings --help',
837 array(
838 'launch' => false,
839 'exit_error' => false,
840 )
841 );
842
843 return;
844 }
845
846 [$command] = $args;
847 switch ( $command ) {
848 case 'reset':
849 WP_CLI::confirm( 'This will completely reset the plugin settings, are you sure to continue?', $options );
850 // Analog Settings > Reset Settings.
851 wd_di()->get( Login_Access::class )->remove_settings();
852 wd_di()->get( Audit_Logging::class )->remove_settings();
853 wd_di()->get( Dashboard::class )->remove_settings();
854 wd_di()->get( Security_Tweaks::class )->remove_settings();
855 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
856 // Parent and submodules.
857 wd_di()->get( \WP_Defender\Controller\Firewall::class )->remove_settings();
858
859 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
860 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
861 wd_di()->get( Two_Factor::class )->remove_settings();
862 wd_di()->get( Main_Setting::class )->remove_settings();
863 WP_CLI::log( 'All cleared!' );
864 break;
865 default:
866 WP_CLI::error( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ), false );
867 WP_CLI::runcommand(
868 'defender settings --help',
869 array(
870 'launch' => false,
871 'exit_error' => false,
872 )
873 );
874 break;
875 }
876 }
877
878 /**
879 * Manage firewall submodules, data, and lockouts via WP-CLI.
880 *
881 * ## OPTIONS
882 *
883 * <command>
884 * : Action to perform.
885 * ---
886 * options:
887 * - clear
888 * - unblock
889 * - list
890 * - activate
891 * - deactivate
892 * ---
893 *
894 * <type>
895 * : The firewall data type to target (e.g. ip, user_agent, files, maxmind, submodule).
896 *
897 * [<field>]
898 * : The specific field or submodule to target. Defaults to 'all' for the list command.
899 *
900 * [--ips=<ips>]
901 * : Comma-separated list of IP addresses to unblock. Required for the unblock command.
902 *
903 * ## EXAMPLES
904 *
905 * # Clear the IP allowlist.
906 * $ wp defender firewall clear ip allowlist
907 * Firewall allowlist ip is cleared.
908 *
909 * # Unblock specific IPs from lockout.
910 * $ wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
911 * Firewall lockout ip unblocked
912 *
913 * # List all user agent entries.
914 * $ wp defender firewall list user_agent all
915 *
916 * # Activate login protection submodule.
917 * $ wp defender firewall activate submodule login_protection
918 * Success: Firewall "Login Protection" has been activated.
919 *
920 * # Deactivate 404 detection submodule.
921 * $ wp defender firewall deactivate submodule 404_detection
922 * Success: Firewall "404 Detection" has been deactivated.
923 *
924 * @param mixed $args Command arguments.
925 * @param mixed $options Command options.
926 */
927 public function firewall( $args, $options ) {
928 $arg_count = is_array( $args ) || $args instanceof Countable ? count( $args ) : 0;
929 if ( $arg_count < 2 ) {
930 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
931 WP_CLI::runcommand(
932 'defender firewall --help',
933 array(
934 'launch' => false,
935 'exit_error' => false,
936 )
937 );
938
939 return;
940 }
941
942 $command = $args[0];
943 $type = $args[1];
944 // Field is optional for the 'list' command — defaults to 'all'.
945 $field = $args[2] ?? ( 'list' === $command ? 'all' : '' );
946
947 if ( ! is_string( $type ) || '' === $type ) {
948 WP_CLI::error( 'Invalid option.', false );
949 WP_CLI::runcommand(
950 'defender firewall --help',
951 array(
952 'launch' => false,
953 'exit_error' => false,
954 )
955 );
956
957 return;
958 }
959 switch ( $command ) {
960 case 'clear':
961 $this->clear_firewall( $type, $field );
962 break;
963 case 'unblock':
964 $this->unblock_firewall( $type, $field, $options );
965 break;
966 case 'list':
967 $this->list_firewall( $type, $field );
968 break;
969 case 'activate':
970 $this->toggle_firewall_submodule( $type, $field, 'activate' );
971 break;
972 case 'deactivate':
973 $this->toggle_firewall_submodule( $type, $field, 'deactivate' );
974 break;
975 default:
976 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
977 break;
978 }
979 }
980
981 /**
982 * Clears the firewall data based on the specified type and field.
983 *
984 * @param string $type The type of data to clear.
985 * @param string $field The specific field to clear.
986 */
987 private function clear_firewall( $type, $field ) {
988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
990
991 if ( ! in_array( $type, $type_default, true ) ) {
992 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
993 WP_CLI::runcommand(
994 'defender firewall --help',
995 array(
996 'launch' => false,
997 'exit_error' => false,
998 )
999 );
1000
1001 return;
1002 }
1003
1004 if ( ! in_array( $field, $field_default, true ) ) {
1005 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1006 WP_CLI::runcommand(
1007 'defender firewall --help',
1008 array(
1009 'launch' => false,
1010 'exit_error' => false,
1011 )
1012 );
1013
1014 return;
1015 }
1016
1017 // Rename the field's name to original model field name.
1018 $original_field = $this->rename_field( $field );
1019 if ( 'ip' === $type ) {
1020 // Get the model instance.
1021 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
1022 $data = $model->export();
1023 // Rename the field to match with the appropriate model field name.
1024 $mod_field = $this->is_country( $original_field ) ? $original_field : 'ip_' . $original_field;
1025 // Reset to default data with correct data type.
1026 $default_data = $this->is_country( $original_field ) ? array() : '';
1027 // Empty the $field option of field data.
1028 $data[ $mod_field ] = $default_data;
1029 $model->import( $data );
1030 $model->save();
1031 } elseif ( 'files' === $type ) {
1032 // Get the model instance.
1033 $model = wd_di()->get( Notfound_Lockout::class );
1034 $data = $model->export();
1035 // Empty the $field option of field data.
1036 $data[ $original_field ] = '';
1037 $model->import( $data );
1038 $model->save();
1039 } elseif ( 'user_agent' === $type ) {
1040 $model = wd_di()->get( User_Agent_Lockout::class );
1041 $data = $model->export();
1042 $data[ $original_field ] = '';
1043 $model->import( $data );
1044 $model->save();
1045 } elseif ( 'maxmind' === $type ) {
1046 try {
1047 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
1048 if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
1049 wp_delete_file( $model->geodb_path );
1050 }
1051 $model->maxmind_license_key = '';
1052 $model->geodb_path = '';
1053 $model->save();
1054 } catch ( Throwable $th ) {
1055 WP_CLI::log( $th->getMessage() );
1056 }
1057 }
1058
1059 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
1060 }
1061
1062 /**
1063 * Rename a field to its original model field name.
1064 *
1065 * @param string $field The field name to rename.
1066 *
1067 * @return string The renamed field name.
1068 */
1069 private function rename_field( $field ) {
1070 if ( '' !== $field ) {
1071 return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
1072 }
1073
1074 return '';
1075 }
1076
1077 /**
1078 * Check if the specified field is related to country settings.
1079 *
1080 * @param string $field The field to check.
1081 *
1082 * @return bool True if the field is related to country settings, false otherwise.
1083 */
1084 private function is_country( $field ) {
1085 return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
1086 }
1087
1088 /**
1089 * Unblocks the specified IPs from the firewall.
1090 *
1091 * @param string $type The type of data to unblock.
1092 * @param string $field The specific field to unblock.
1093 * @param array $options Command options including IPs to unblock.
1094 */
1095 private function unblock_firewall( $type, $field, $options ) {
1096 $type_default = array( 'ip' );
1097 $field_default = array( 'lockout' );
1098
1099 if ( ! in_array( $type, $type_default, true ) ) {
1100 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
1101 WP_CLI::runcommand(
1102 'defender firewall --help',
1103 array(
1104 'launch' => false,
1105 'exit_error' => false,
1106 )
1107 );
1108
1109 return;
1110 }
1111
1112 if ( ! in_array( $field, $field_default, true ) ) {
1113 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1114 WP_CLI::runcommand(
1115 'defender firewall --help',
1116 array(
1117 'launch' => false,
1118 'exit_error' => false,
1119 )
1120 );
1121
1122 return;
1123 }
1124
1125 if ( array_key_exists( 'ips', $options ) ) {
1126 $ips = array_map( 'trim', explode( ',', $options['ips'] ) );
1127 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
1128
1129 foreach ( $models as $model ) {
1130 $model->status = Lockout_Ip::STATUS_NORMAL;
1131 $model->save();
1132 }
1133 } else {
1134 WP_CLI::error( 'Option \'ips\' is not provided. See below...', false );
1135 WP_CLI::runcommand(
1136 'defender firewall --help',
1137 array(
1138 'launch' => false,
1139 'exit_error' => false,
1140 )
1141 );
1142
1143 return;
1144 }
1145
1146 WP_CLI::log( sprintf( 'Firewall %s %s unblocked', str_replace( '_', ' ', $field ), $type ) );
1147 }
1148
1149 /**
1150 * Lists details for the firewall based on the specified type and field.
1151 * Example: wp defender firewall list user_agent all
1152 *
1153 * @param string $type The type of data to list.
1154 * @param string $field The specific field to list.
1155 *
1156 * @since v2.6.4. Add the details for User Agent Banning.
1157 */
1158 private function list_firewall( $type, $field ) {
1159 $type_default = array( 'user_agent' );
1160 $field_default = array( 'all', 'allowlist', 'blocklist' );
1161 if ( ! in_array( $type, $type_default, true ) ) {
1162 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
1163 WP_CLI::runcommand(
1164 'defender firewall --help',
1165 array(
1166 'launch' => false,
1167 'exit_error' => false,
1168 )
1169 );
1170
1171 return;
1172 }
1173 if ( ! in_array( $field, $field_default, true ) ) {
1174 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1175 WP_CLI::runcommand(
1176 'defender firewall --help',
1177 array(
1178 'launch' => false,
1179 'exit_error' => false,
1180 )
1181 );
1182
1183 return;
1184 }
1185 $model = wd_di()->get( User_Agent_Lockout::class );
1186 $data = $model->export();
1187 if ( 'all' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
1188 WP_CLI::log( 'ALLOWLIST:' );
1189 WP_CLI::log( $data['whitelist'] );
1190 WP_CLI::log( 'BLOCKLIST:' );
1191 WP_CLI::log( $data['blacklist'] );
1192 } elseif ( 'allowlist' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] ) {
1193 WP_CLI::log( $data['whitelist'] );
1194 } elseif ( 'blocklist' === $field && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
1195 WP_CLI::log( $data['blacklist'] );
1196 } else {
1197 WP_CLI::log( 'No data.' );
1198 }
1199 }
1200
1201 /**
1202 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
1203 * Example: wp defender firewall activate submodule user_agent
1204 * Example: wp defender firewall deactivate submodule login_protection
1205 *
1206 * @param string $key_word The keyword to identify the action.
1207 * @param string $submodule The submodule to toggle.
1208 * @param string $action The action to perform (activate or deactivate).
1209 */
1210 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
1211 if ( 'submodule' !== $key_word ) {
1212 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $key_word ), false );
1213 WP_CLI::runcommand(
1214 'defender firewall --help',
1215 array(
1216 'launch' => false,
1217 'exit_error' => false,
1218 )
1219 );
1220
1221 return;
1222 }
1223 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
1224 WP_CLI::error( sprintf( 'Invalid option %s. See below...', $submodule ), false );
1225 WP_CLI::runcommand(
1226 'defender firewall --help',
1227 array(
1228 'launch' => false,
1229 'exit_error' => false,
1230 )
1231 );
1232
1233 return;
1234 }
1235 // Get submodule slug.
1236 if ( 'login_protection' === $submodule ) {
1237 $model = wd_di()->get( Login_Lockout::class );
1238 $submodule = Login_Lockout::get_module_name();
1239 } elseif ( '404_detection' === $submodule ) {
1240 $model = wd_di()->get( Notfound_Lockout::class );
1241 $submodule = Notfound_Lockout::get_module_name();
1242 } else {
1243 $model = wd_di()->get( User_Agent_Lockout::class );
1244 $submodule = User_Agent_Lockout::get_module_name();
1245 }
1246 // Activate/deactivate submodule.
1247 if ( 'activate' === $action ) {
1248 $text = 'activated';
1249 // Check if the submodule is not yet activated.
1250 if ( true !== $model->enabled ) {
1251 $model->enabled = true;
1252 $model->save();
1253 }
1254 } else {
1255 $text = 'deactivated';
1256 // Check if the submodule is not yet deactivated.
1257 if ( false !== $model->enabled ) {
1258 $model->enabled = false;
1259 $model->save();
1260 }
1261 }
1262
1263 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
1264 }
1265
1266 /**
1267 * Check if the testing mode is enabled.
1268 * Outputs an error and returns false if WP_DEFENDER_TESTING is not defined and true.
1269 *
1270 * @return bool
1271 */
1272 private function is_testing_mode(): bool {
1273 if ( ! defined( 'WP_DEFENDER_TESTING' ) || ! WP_DEFENDER_TESTING ) {
1274 WP_CLI::error( 'This command is intended for testing only. Define WP_DEFENDER_TESTING as true to proceed.' );
1275
1276 return false;
1277 }
1278
1279 return true;
1280 }
1281
1282 /**
1283 * Force Bulk Password Reset.
1284 * <command>
1285 * : Action to perform.
1286 * ---
1287 * options:
1288 * - clear
1289 * ---
1290 *
1291 * ## EXAMPLES
1292 *
1293 * # Reset all mask login settings to defaults.
1294 * $ wp defender mask_login clear
1295 * Mask login settings cleared!
1296 *
1297 * @param mixed $args Command arguments.
1298 */
1299 public function mask_login( $args ) {
1300 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1301 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
1302 WP_CLI::runcommand(
1303 'defender mask_login --help',
1304 array(
1305 'launch' => false,
1306 'exit_error' => false,
1307 )
1308 );
1309
1310 return;
1311 }
1312
1313 [$command] = $args;
1314 switch ( $command ) {
1315 case 'clear':
1316 wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
1317 WP_CLI::log( 'Mask login settings cleared!' );
1318 break;
1319 default:
1320 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1321 break;
1322 }
1323 }
1324
1325 /**
1326 * Manage bulk password reset via WP-CLI.
1327 *
1328 * ## OPTIONS
1329 *
1330 * <command>
1331 * : Action to perform.
1332 * ---
1333 * options:
1334 * - force
1335 * - undo
1336 * ---
1337 *
1338 * ## EXAMPLES
1339 *
1340 * # Force all users to reset their password on next login.
1341 * $ wp defender password_reset force
1342 *
1343 * # Cancel a previously forced password reset.
1344 * $ wp defender password_reset undo
1345 * Passwords reset is no longer required.
1346 *
1347 * @param mixed $args Command arguments.
1348 */
1349 public function password_reset( $args ) {
1350 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1351 WP_CLI::error( 'Invalid command.' );
1352
1353 return;
1354 }
1355
1356 [$command] = $args;
1357 switch ( $command ) {
1358 case 'force':
1359 // Get the model instance.
1360 $model = wd_di()->get( Password_Reset::class );
1361 $model->expire_force = true;
1362 $model->force_time = time();
1363 $model->save();
1364 $message = sprintf( 'Passwords created before %s are required to be reset upon next login.', $this->format_date_time( $model->force_time ) );
1365 WP_CLI::log( $message );
1366 break;
1367 case 'undo':
1368 $model = wd_di()->get( Password_Reset::class );
1369 $model->expire_force = false;
1370 $model->save();
1371 WP_CLI::log( 'Passwords reset is no longer required.' );
1372 break;
1373 default:
1374 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1375 break;
1376 }
1377 }
1378
1379 /**
1380 * Manage Defender's internal log files.
1381 *
1382 * ## OPTIONS
1383 *
1384 * <command>
1385 * : Action to perform.
1386 * ---
1387 * options:
1388 * - delete
1389 * ---
1390 *
1391 * ## EXAMPLES
1392 *
1393 * # Delete log files older than one week.
1394 * $ wp defender logs delete
1395 * Logs older than a week have been deleted.
1396 *
1397 * @param mixed $args Command arguments.
1398 */
1399 public function logs( $args ) {
1400 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1401 WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
1402 WP_CLI::runcommand(
1403 'defender logs --help',
1404 array(
1405 'launch' => false,
1406 'exit_error' => false,
1407 )
1408 );
1409
1410 return;
1411 }
1412
1413 [$command] = $args;
1414
1415 switch ( $command ) {
1416 case 'delete':
1417 $rotation_logger = wd_di()->get( Rotation_Logger::class );
1418 $rotation_logger->purge_old_log();
1419 WP_CLI::log( 'Logs older than a week have been deleted.' );
1420 break;
1421 default:
1422 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1423 break;
1424 }
1425 }
1426
1427 /**
1428 * Manage CAPTCHA settings via WP-CLI.
1429 *
1430 * ## OPTIONS
1431 *
1432 * <command>
1433 * : Action to perform.
1434 * ---
1435 * options:
1436 * - activate
1437 * - deactivate
1438 * - clear
1439 * ---
1440 *
1441 * ## EXAMPLES
1442 *
1443 * # Enable CAPTCHA.
1444 * $ wp defender captcha activate
1445 * CAPTCHA is activated.
1446 *
1447 * # Disable CAPTCHA.
1448 * $ wp defender captcha deactivate
1449 * CAPTCHA is deactivated.
1450 *
1451 * # Reset all CAPTCHA settings to defaults.
1452 * $ wp defender captcha clear
1453 * CAPTCHA is cleared.
1454 *
1455 * @param mixed $args Command arguments.
1456 */
1457 public function captcha( $args ) {
1458 if ( ! is_array( $args ) || array() === $args ) {
1459 WP_CLI::error( 'Invalid command.' );
1460
1461 return;
1462 }
1463 $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1464 [$command] = $args;
1465 switch ( $command ) {
1466 case 'activate':
1467 if ( true !== $model->enabled ) {
1468 $model->enabled = true;
1469 $model->save();
1470 }
1471 WP_CLI::log( 'CAPTCHA is activated.' );
1472 break;
1473 case 'deactivate':
1474 if ( false !== $model->enabled ) {
1475 $model->enabled = false;
1476 $model->save();
1477 }
1478 WP_CLI::log( 'CAPTCHA is deactivated.' );
1479 break;
1480 case 'clear':
1481 $default_values = $model->get_default_values();
1482 $model->message = $default_values['message'];
1483 $model->language = 'automatic';
1484 $model->provider = 'recaptcha';
1485 $model->data_v2_checkbox = array(
1486 'key' => '',
1487 'secret' => '',
1488 'size' => 'normal',
1489 'style' => 'light',
1490 );
1491 $model->data_v2_invisible = array(
1492 'key' => '',
1493 'secret' => '',
1494 );
1495 $model->data_v3_recaptcha = array(
1496 'key' => '',
1497 'secret' => '',
1498 'threshold' => '0.5',
1499 );
1500 $model->data_turnstile = array(
1501 'key' => '',
1502 'secret' => '',
1503 'size' => 'normal',
1504 'style' => 'auto',
1505 'message' => $default_values['turnstile_message'],
1506 'language' => 'auto',
1507 );
1508 $model->locations = array();
1509 $model->detect_woo = false;
1510 $model->woo_checked_locations = array();
1511 $model->detect_buddypress = false;
1512 $model->buddypress_checked_locations = array();
1513 $model->disable_for_known_users = true;
1514 $model->save();
1515
1516 WP_CLI::log( 'CAPTCHA is cleared.' );
1517 break;
1518 default:
1519 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1520 break;
1521 }
1522 }
1523 }
1524