PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 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 All 141 releases
← All changes | src/component/class-cli.php +830 -367 5.5.1 → 6.3.0 View file →
@@ -8,15 +8,20 @@
8 8
9 9 namespace WP_Defender\Component;
10 10
11 11 use WP_CLI;
12 +use Countable;
12 13 use Exception;
13 -use Countable;
14 14 use Throwable;
15 15 use Faker\Factory;
16 +use WP_Filesystem_Base;
16 17 use WP_CLI\ExitException;
18 +use WP_Defender\Traits\IO;
17 19 use WP_Defender\Traits\Theme;
20 +use WP_Defender\Traits\Plugin;
18 21 use WP_Defender\Traits\Formats;
22 +use WP_Defender\Behavior\WPMUDEV;
23 +use WP_Defender\Component\Audit;
19 24 use WP_Defender\Model\Audit_Log;
20 25 use WP_Defender\Model\Scan_Item;
21 26 use WP_Defender\Model\Lockout_Ip;
22 27 use WP_Defender\Model\Lockout_Log;
@@ -21,20 +26,22 @@
21 26 use WP_Defender\Model\Lockout_Ip;
22 27 use WP_Defender\Model\Lockout_Log;
23 28 use WP_Defender\Controller\Dashboard;
24 29 use WP_Defender\Controller\Two_Factor;
25 -use WP_Defender\Model\Scan as Model_Scan;
30 +use WP_Defender\Controller\Login_Access;
26 31 use WP_Defender\Controller\Main_Setting;
27 32 use WP_Defender\Controller\Audit_Logging;
28 -use WP_Defender\Controller\Advanced_Tools;
33 +use WP_Defender\Model\Scan as Model_Scan;
29 34 use WP_Defender\Controller\Security_Tweaks;
30 35 use WP_Defender\Model\Setting\Login_Lockout;
36 +use WP_Defender\Behavior\Scan\Core_Integrity;
37 +use WP_Defender\Controller\Blocklist_Monitor;
31 38 use WP_Defender\Model\Setting\Password_Reset;
32 -use WP_Defender\Controller\Blocklist_Monitor;
33 39 use WP_Defender\Model\Setting\Notfound_Lockout;
34 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;
35 43 use WP_Defender\Model\Setting\User_Agent_Lockout;
36 -use WP_Defender\Component\Logger\Rotation_Logger;
37 44 use function WP_CLI\Utils\format_items;
38 45
39 46 if ( ! defined( 'WPINC' ) ) {
40 47 die;
@@ -56,33 +63,109 @@
56 63 local_to_utc as protected;
57 64 moment_datetime_format_from as protected;
58 65 persistent_hub_datetime_format as protected;
59 66 time_since as protected;
67 + get_local_human_date as protected;
68 + get_time_diff as protected;
60 69 }
61 - use Theme;
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 + }
62 104
63 105 /**
64 - * This is a helper for scan module.
65 - * #Options
106 + * Run scans and manage scan results via WP-CLI.
107 + *
108 + * ## OPTIONS
109 + *
66 110 * <command>
67 - * : Value can be run - Perform a scan, e.g. 'run'-command or 'run ----type=detailed' for detailed result,
68 - * or (un)ignore|delete|resolve to do the relevant task,
69 - * or clear_logs to remove completed schedule logs.
111 + * : Action to perform.
112 + * ---
113 + * options:
114 + * - run
115 + * - ignore
116 + * - unignore
117 + * - resolve
118 + * - delete
119 + * - clear_logs
120 + * ---
121 + *
70 122 * [--type=<type>]
71 - * : Default, without values, is for all items, or core_integrity|plugin_integrity|vulnerability|suspicious_code.
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 + * ---
72 134 *
73 - * @param mixed $args Command arguments.
74 - * @param mixed $options Command options.
135 + * ## EXAMPLES
75 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 + *
76 159 * @throws ExitException If an invalid command is provided.
77 160 */
78 161 public function scan( $args, $options ) {
79 - if ( empty( $args ) ) {
162 + if ( ! is_array( $args ) || array() === $args ) {
80 163 WP_CLI::error( 'Invalid command' );
81 164
82 165 return;
83 166 }
84 - [ $command ] = $args;
167 + [$command] = $args;
85 168 switch ( $command ) {
86 169 case 'run':
87 170 $this->scan_all( $options );
88 171 break;
@@ -89,19 +172,11 @@
89 172 case 'clear_logs':
90 173 $this->scan_clear_logs();
91 174 break;
92 175 default:
93 - $commands = array(
94 - 'ignore',
95 - 'unignore',
96 - 'resolve',
97 - 'delete',
98 - );
176 + $commands = array( 'ignore', 'unignore', 'resolve', 'delete' );
99 177 if ( in_array( $command, $commands, true ) ) {
100 - WP_CLI::confirm(
101 - 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?',
102 - $options
103 - );
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 );
104 179 $this->scan_task( $command, $options );
105 180 } else {
106 181 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
107 182 }
@@ -109,15 +184,106 @@
109 184 }
110 185 }
111 186
112 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 + /**
113 265 * Executes tasks based on the type of scan.
114 266 *
115 - * @param mixed $task The task to perform.
116 - * @param mixed $options Command options.
267 + * @param string $command The task to perform.
268 + * @param mixed $options Command options.
117 269 */
118 - private function scan_task( $task, $options ) {
119 - $type = $options['type'] ?? null;
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 +
120 286 switch ( $type ) {
121 287 case null:
122 288 // All items.
123 289 $type = null;
@@ -133,9 +299,14 @@
133 299 break;
134 300 case 'suspicious_code':
135 301 $type = Scan_Item::TYPE_SUSPICIOUS;
136 302 break;
137 - // Todo: add Abandoned type.
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;
138 309 default:
139 310 WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
140 311 break;
141 312 }
@@ -140,20 +311,22 @@
140 311 break;
141 312 }
142 313 $active = Model_Scan::get_active();
143 314 if ( is_object( $active ) ) {
144 - return WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
315 + WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
145 316 }
146 317 $model = Model_Scan::get_last();
147 318 if ( ! is_object( $model ) ) {
148 319 return;
149 320 }
150 - switch ( $task ) {
321 + switch ( $command ) {
151 322 case 'ignore':
152 323 $issues = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
153 324 foreach ( $issues as $issue ) {
154 - $model->ignore_issue( $issue->id );
155 - WP_CLI::log( sprintf( 'Ignoring file: %s', $issue->raw_data['file'] ) );
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 + }
156 329 }
157 330 WP_CLI::log( sprintf( 'Ignored %s items', count( $issues ) ) );
158 331 break;
159 332 case 'unignore':
@@ -158,10 +331,12 @@
158 331 break;
159 332 case 'unignore':
160 333 $issues = $model->get_issues( $type, Scan_Item::STATUS_IGNORE );
161 334 foreach ( $issues as $issue ) {
162 - $model->unignore_issue( $issue->id );
163 - WP_CLI::log( sprintf( 'Unignoring file: %s', $issue->raw_data['file'] ) );
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 + }
164 339 }
165 340 WP_CLI::log( sprintf( 'Unignored %s items', count( $issues ) ) );
166 341 break;
167 342 case 'resolve':
@@ -167,38 +342,28 @@
167 342 case 'resolve':
168 343 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
169 344 $resolved = array();
170 345 foreach ( $items as $item ) {
171 - if (
172 - in_array(
173 - $item->type,
174 - array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ),
175 - true
176 - )
177 - ) {
346 + if ( in_array( $item->type, array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ), true ) ) {
178 347 WP_CLI::log( sprintf( 'Reverting %s to original', $item->raw_data['file'] ) );
179 348 $ret = $item->resolve();
180 349 if ( ! is_wp_error( $ret ) ) {
181 350 $resolved[] = $item;
182 351 } else {
183 - return WP_CLI::error( $ret->get_error_message() );
352 + WP_CLI::error( $ret->get_error_message() );
184 353 }
185 354 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $item->type ) {
186 355 // If this is content, we will try to delete them.
187 - $whitelist = array(
188 - // wordfence waf.
189 - ABSPATH . '/wordfence-waf.php',
190 - // Any files inside plugins, if removed, can cause fatal error.
191 - WP_CONTENT_DIR . '/plugins/',
192 - // Any files inside themes.
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.
193 359 $this->get_path_of_themes_dir(),
194 360 );
195 361 $path = $item->raw_data['file'];
196 362 $can_delete = true;
197 - $current = '';
198 363 foreach ( $whitelist as $value ) {
199 364 $current = $value;
200 - if ( strpos( $value, $path ) > 0 ) {
365 + if ( str_contains( $path, $value ) ) {
201 366 // Ignore this.
202 367 $can_delete = false;
203 368 break;
204 369 }
@@ -204,15 +369,25 @@
204 369 }
205 370 }
206 371 if ( false === $can_delete ) {
207 372 WP_CLI::log( sprintf( 'Ignore file %s as it is in %s', $path, $current ) );
208 - } elseif ( wp_delete_file( $path ) ) {
373 + } elseif ( ! is_dir( $path ) && wp_delete_file( $path ) ) {
209 374 WP_CLI::log( sprintf( 'Delete file %s', $path ) );
210 375 $model->remove_issue( $item->id );
211 376 $resolved[] = $item;
212 377 } else {
213 - return WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
378 + WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
214 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 + }
215 390 }
216 391 }
217 392 WP_CLI::log( sprintf( 'Resolved %s items', count( $resolved ) ) );
218 393 break;
@@ -219,16 +394,45 @@
219 394 case 'delete':
220 395 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
221 396 $deleted = array();
222 397 foreach ( $items as $item ) {
223 - $path = $item->raw_data['file'];
224 - if ( wp_delete_file( $path ) ) {
225 - WP_CLI::log( sprintf( 'Delete file %s', $path ) );
226 - $model->remove_issue( $item->id );
227 - $deleted[] = $item;
228 - } else {
229 - return WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
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;
230 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 + }
231 435 }
232 436 WP_CLI::log( sprintf( 'Deleted %s items', count( $deleted ) ) );
233 437 break;
234 438 default:
@@ -236,76 +440,105 @@
236 440 }
237 441 }
238 442
239 443 /**
240 - * Generate dummy data, use in cypress & unit test.
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.
241 492 * DO NOT USE IN PRODUCTION.
242 493 *
243 - * @param mixed $args Command arguments.
494 + * @param mixed $args Command arguments.
244 495 */
245 496 public function seed( $args ) {
246 497 global $wp_filesystem;
247 498 // Initialize the WP filesystem, no more using 'file-put-contents' function.
248 - if ( empty( $wp_filesystem ) ) {
499 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
249 500 require_once ABSPATH . '/wp-admin/includes/file.php';
250 501 WP_Filesystem();
251 502 }
252 - if ( empty( $args ) ) {
503 + if ( ! is_array( $args ) || array() === $args ) {
253 504 WP_CLI::error( 'Invalid command' );
254 505
255 506 return;
256 507 }
508 + if ( ! $this->is_testing_mode() ) {
509 + return;
510 + }
511 +
257 512 [ $command ] = $args;
258 513 switch ( $command ) {
259 514 case 'scan:core':
515 + WP_CLI::confirm( 'This will modify a WordPress core file (wp-load.php). Are you sure?', array() );
516 +
260 517 $file_path = ABSPATH . 'wp-load.php';
261 - $content = '//this make different';
262 - if ( $wp_filesystem->exists( $file_path ) ) {
263 - $content = $wp_filesystem->get_contents( $file_path ) . $content;
518 + if ( ! $wp_filesystem->exists( $file_path ) ) {
519 + WP_CLI::error( sprintf( 'File does not exist: %s', $file_path ) );
520 +
521 + return;
264 522 }
265 - $wp_filesystem->put_contents( $file_path, $content );
266 - break;
267 - case 'audit:logs':
268 - $types = array(
269 - Audit_Log::EVENT_TYPE_USER,
270 - Audit_Log::EVENT_TYPE_SYSTEM,
271 - Audit_Log::EVENT_TYPE_COMMENT,
272 - Audit_Log::EVENT_TYPE_MEDIA,
273 - Audit_Log::EVENT_TYPE_SETTINGS,
274 - Audit_Log::EVENT_TYPE_CONTENT,
275 - Audit_Log::EVENT_TYPE_MENU,
276 - );
277 - $faker = Factory::create();
278 - for ( $i = 0; $i < 500; $i++ ) {
279 - $log = new Audit_Log();
280 - $log->timestamp = Crypt::random_int( strtotime( '-31 days' ), time() );
281 - $log->event_type = $types[ array_rand( $types ) ];
282 - $log->action_type = $faker->word();
283 - $log->site_url = $faker->url();
284 - $log->user_id = $faker->numberBetween( 1, 1000 );
285 - $log->context = $faker->word();
286 - $log->ip = $faker->ipv4();
287 - $log->msg = $faker->word();
288 - $log->blog_id = $faker->numberBetween( 1, 100 );
289 - $log->synced = $faker->numberBetween( 0, 1 );
290 - $log->ttl = $faker->numberBetween( 1, 3600 );
291 - $log->save();
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;
292 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' );
293 535 break;
294 536 case 'ip:logs':
537 + WP_CLI::confirm( 'This will insert fake firewall lockout log entries into the database. Are you sure?', array() );
295 538 // We will generate randomly 10k logs in 3 months.
296 - $types = array(
297 - Lockout_Log::AUTH_FAIL,
298 - Lockout_Log::AUTH_LOCK,
299 - Lockout_Log::ERROR_404,
300 - Lockout_Log::LOCKOUT_404,
301 - Lockout_Log::LOCKOUT_UA,
302 - );
303 - $is_lock = array(
304 - Lockout_Log::AUTH_LOCK,
305 - Lockout_Log::LOCKOUT_404,
306 - Lockout_Log::LOCKOUT_UA,
307 - );
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 );
308 541 $faker = Factory::create();
309 542 WP_CLI::log( $faker->ipv4 );
310 543 $range = array(
311 544 'today midnight' => array( 'now', 100 ),
@@ -320,9 +553,9 @@
320 553 'ua_lockout' => 0,
321 554 );
322 555 $last_lockout = 0;
323 556 foreach ( $range as $date => $to ) {
324 - [ $to, $count ] = $to;
557 + [$to, $count] = $to;
325 558 for ( $i = 0; $i < $count; $i++ ) {
326 559 $model = new Lockout_Log();
327 560 $model->ip = $faker->ipv4;
328 561 $model->type = $types[ array_rand( $types ) ];
@@ -362,30 +595,54 @@
362 595 }
363 596
364 597 /**
365 598 * Clean up dummy data.
599 + * DO NOT USE IN PRODUCTION.
366 600 *
367 - * @param mixed $args Command arguments.
601 + * @param mixed $args Command arguments.
368 602 */
369 603 public function unseed( $args ) {
370 604 global $wp_filesystem;
371 605 // Initialize the WP filesystem, no more using 'file-put-contents' function.
372 - if ( empty( $wp_filesystem ) ) {
606 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
373 607 require_once ABSPATH . '/wp-admin/includes/file.php';
374 608 WP_Filesystem();
375 609 }
376 - if ( empty( $args ) ) {
610 + if ( ! is_array( $args ) || array() === $args ) {
377 611 WP_CLI::error( 'Invalid command' );
378 612
379 613 return;
380 614 }
615 + if ( ! $this->is_testing_mode() ) {
616 + return;
617 + }
618 +
381 619 [ $command ] = $args;
382 620 switch ( $command ) {
383 621 case 'scan:core':
384 - $content = file_get_contents( ABSPATH . 'wp-load.php' );
385 - $wp_filesystem->put_contents( ABSPATH . 'wp-load.php', str_replace( '//this make different', '', $content ) );
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 ) );
386 642 break;
387 643 case 'scan:suspicious':
644 + WP_CLI::confirm( 'This will delete the false-positive test file. Are you sure?', array() );
388 645 wp_delete_file( WP_CONTENT_DIR . '/false-positive.php' );
389 646 break;
390 647 default:
391 648 break;
@@ -392,126 +649,106 @@
392 649 }
393 650 }
394 651
395 652 /**
396 - * Clears the audit log from Database.
397 - * <command> reset
398 - * This command must have this command
399 - * Syntax: wp defender audit <command>
400 - * Example: wp defender audit reset
653 + * Manage audit logs via WP-CLI.
401 654 *
402 - * @param mixed $args Command arguments.
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.
403 676 */
404 677 public function audit( $args ) {
405 - if ( empty( $args ) ) {
406 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
407 - WP_CLI::runcommand( 'defender audit --help' );
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 + );
408 687
409 688 return;
410 689 }
411 - [ $command ] = $args;
690 +
691 + [$command] = $args;
412 692 switch ( $command ) {
413 693 case 'reset':
414 - Audit_Log::truncate();
415 - delete_site_option( 'wd_audit_fetch_checkpoint' );
694 + wd_di()->get( Audit::class )->reset();
416 695
417 696 WP_CLI::log( 'All clear' );
418 697 break;
419 698 default:
420 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
421 - WP_CLI::runcommand( 'defender audit --help' );
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 + );
422 707 break;
423 708 }
424 709 }
425 710
426 711 /**
427 - * Starts a full scan based on the provided options.
712 + * Manage security headers via WP-CLI.
428 713 *
429 - * @param array $options Command options.
430 - */
431 - private function scan_all( $options ) {
432 - $type = $options['type'] ?? null;
433 - $is_detailed = false;
434 - switch ( $type ) {
435 - case null:
436 - // All items.
437 - $type = null;
438 - break;
439 - case 'detailed':
440 - $is_detailed = true;
441 - break;
442 - default:
443 - WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
444 - break;
445 - }
446 - WP_CLI::log( 'Check if there is a scan ongoing...' );
447 - $scan = Model_Scan::get_active();
448 - if ( ! is_object( $scan ) ) {
449 - WP_CLI::log( 'No active scan, creating...' );
450 - $scan = Model_Scan::create();
451 - if ( is_wp_error( $scan ) ) {
452 - return WP_CLI::error( $scan->get_error_message() );
453 - }
454 - } else {
455 - WP_CLI::log( 'Continue from last scan' );
456 - }
457 - // Start detailed scan.
458 - if ( $is_detailed ) {
459 - $start = microtime( true );
460 - }
461 - $handler = wd_di()->get( Scan::class );
462 - $ret = false;
463 - while ( $handler->process() === false ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedWhile
464 - }
465 - $scan = Model_Scan::get_last();
466 - if ( ! is_object( $scan ) || is_wp_error( $scan ) ) {
467 - return;
468 - }
469 - $results = $scan->to_array();
470 - if ( is_array( $results ) && ! empty( $results['issues_items'] ) ) {
471 - $count = is_array( $results['issues_items'] ) || $results['issues_items'] instanceof Countable
472 - ? count( $results['issues_items'] )
473 - : 0;
474 - // Finish detailed scan.
475 - if ( $is_detailed ) {
476 - format_items( 'table', $results['issues_items'], array( 'type', 'short_desc', 'full_path' ) );
477 - WP_CLI::log( sprintf( 'Saved %d items.', $count ) );
478 - $finish = microtime( true ) - $start;
479 - WP_CLI::log( 'Scan takes ' . round( $finish, 2 ) . 's to process.' );
480 - } else {
481 - WP_CLI::log( sprintf( 'Found %d issues.', $count ) );
482 - }
483 - }
484 - WP_CLI::success( 'All done!' );
485 - }
486 -
487 - /**
488 - * This is a helper for Security header actions.
489 - * #Options
714 + * ## OPTIONS
715 + *
490 716 * <command>
491 - * : Value can be run - Check headers, or activate|deactivate all headers
492 - * [--type=<type>]
493 - * : Default is all
717 + * : Action to perform.
718 + * ---
719 + * options:
720 + * - check
721 + * - activate
722 + * - deactivate
723 + * ---
724 + *
494 725 * ## EXAMPLES
495 - * wp defender security_headers check
496 726 *
497 - * @param mixed $args Command arguments.
727 + * # Check the current status of all security headers.
728 + * $ wp defender security_headers check
729 + * Success: Checking is ready.
498 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 + *
499 741 * @throws ExitException|Exception If an invalid command is provided.
500 742 */
501 743 public function security_headers( $args ) {
502 - if ( empty( $args ) ) {
744 + if ( ! is_array( $args ) || array() === $args ) {
503 745 WP_CLI::error( 'Invalid command.' );
504 746
505 747 return;
506 748 }
507 - $model = new Security_Headers();
508 - if ( ! is_object( $model ) ) {
509 - WP_CLI::error( 'Invalid model.' );
510 -
511 - return;
512 - }
513 - [ $command ] = $args;
749 + $model = new Security_Headers();
750 + [$command] = $args;
514 751 switch ( $command ) {
515 752 case 'check':
516 753 $i = 1;
517 754 foreach ( $model->get_headers() as $header ) {
@@ -521,17 +758,17 @@
521 758 }
522 759 WP_CLI::success( 'Checking is ready.' );
523 760 break;
524 761 case 'activate':
525 - foreach ( $model->get_headers() as $header ) {
526 - $model->{$header::$rule_slug} = true;
762 + foreach ( $model->get_headers() as $rule_slug => $header ) {
763 + $this->set_security_header_state( $model, $rule_slug, true );
527 764 }
528 765 $model->save();
529 766 WP_CLI::log( 'Activating is ready.' );
530 767 break;
531 768 case 'deactivate':
532 - foreach ( $model->get_headers() as $header ) {
533 - $model->{$header::$rule_slug} = false;
769 + foreach ( $model->get_headers() as $rule_slug => $header ) {
770 + $this->set_security_header_state( $model, $rule_slug, false );
534 771 }
535 772 $model->save();
536 773 WP_CLI::log( 'Deactivating is ready.' );
537 774 break;
@@ -541,35 +778,78 @@
541 778 }
542 779 }
543 780
544 781 /**
545 - * This is a helper command to reset plugin settings.
546 - * #Options
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 + *
547 816 * <command>
548 - * Only allowed value is reset.
549 - * Syntax: wp defender settings <command>
550 - * Example: wp defender settings reset
817 + * : Action to perform.
818 + * ---
819 + * options:
820 + * - reset
821 + * ---
551 822 *
552 - * @param mixed $args Command arguments.
553 - * @param mixed $options Command options.
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.
554 831 */
555 832 public function settings( $args, $options ) {
556 - if ( empty( $args ) ) {
557 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
558 - WP_CLI::runcommand( 'defender settings --help' );
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 + );
559 842
560 843 return;
561 844 }
562 845
563 - [ $command ] = $args;
846 + [$command] = $args;
564 847 switch ( $command ) {
565 848 case 'reset':
566 - WP_CLI::confirm(
567 - 'This will completely reset the plugin settings, are you sure to continue?',
568 - $options
569 - );
849 + WP_CLI::confirm( 'This will completely reset the plugin settings, are you sure to continue?', $options );
570 850 // Analog Settings > Reset Settings.
571 - wd_di()->get( Advanced_Tools::class )->remove_settings();
851 + wd_di()->get( Login_Access::class )->remove_settings();
572 852 wd_di()->get( Audit_Logging::class )->remove_settings();
573 853 wd_di()->get( Dashboard::class )->remove_settings();
574 854 wd_di()->get( Security_Tweaks::class )->remove_settings();
575 855 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
@@ -578,50 +858,103 @@
578 858
579 859 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
580 860 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
581 861 wd_di()->get( Two_Factor::class )->remove_settings();
582 - wd_di()->get( Blocklist_Monitor::class )->remove_settings();
583 862 wd_di()->get( Main_Setting::class )->remove_settings();
584 863 WP_CLI::log( 'All cleared!' );
585 864 break;
586 865 default:
587 - WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
588 - WP_CLI::runcommand( 'defender settings --help' );
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 + );
589 874 break;
590 875 }
591 876 }
592 877
593 878 /**
594 - * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
595 - * Syntax: wp defender firewall <command> <args_1> <args_2>
596 - * <command> clear|unblock|list|activate|deactivate
879 + * Manage firewall submodules, data, and lockouts via WP-CLI.
597 880 *
598 - * <args_1> Allowed values are: ip, user_agent, files and maxmind
599 - * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
600 - * Example: wp defender firewall clear ip allowlist
601 - * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
602 - * Example: wp defender firewall list user_agent <status>
603 - * Example: wp defender firewall activate submodule <submodule>
604 - * Example: wp defender firewall deactivate submodule login_protection
605 - * <status> Allowed values are: all, allowlist, blocklist.
606 - * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
881 + * ## OPTIONS
607 882 *
608 - * @param mixed $args Command arguments.
609 - * @param mixed $options Command options.
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.
610 926 */
611 927 public function firewall( $args, $options ) {
612 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
613 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
614 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
615 938
616 939 return;
617 940 }
618 941
619 - [ $command, $type, $field ] = $args;
620 - if ( empty( $type ) || empty( $field ) ) {
621 - WP_CLI::log( 'Invalid option.' );
622 - WP_CLI::runcommand( 'defender firewall --help' );
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' : '' );
623 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 +
624 957 return;
625 958 }
626 959 switch ( $command ) {
627 960 case 'clear':
@@ -645,41 +978,12 @@
645 978 }
646 979 }
647 980
648 981 /**
649 - * This clears the mask login settings.
650 - * <command> clear
651 - * This command must have this command
652 - * Syntax: wp defender mask_login <command>
653 - * Example: wp defender mask_login clear
654 - *
655 - * @param mixed $args Command arguments.
656 - */
657 - public function mask_login( $args ) {
658 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
659 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
660 - WP_CLI::runcommand( 'defender mask_login --help' );
661 -
662 - return;
663 - }
664 -
665 - [ $command ] = $args;
666 - switch ( $command ) {
667 - case 'clear':
668 - wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
669 - WP_CLI::log( 'Mask login settings cleared!' );
670 - break;
671 - default:
672 - WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
673 - break;
674 - }
675 - }
676 -
677 - /**
678 982 * Clears the firewall data based on the specified type and field.
679 983 *
680 - * @param string $type The type of data to clear.
681 - * @param string $field The specific field to clear.
984 + * @param string $type The type of data to clear.
985 + * @param string $field The specific field to clear.
682 986 */
683 987 private function clear_firewall( $type, $field ) {
684 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
685 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
@@ -684,17 +988,29 @@
684 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
685 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
686 990
687 991 if ( ! in_array( $type, $type_default, true ) ) {
688 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
689 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
690 1000
691 1001 return;
692 1002 }
693 1003
694 1004 if ( ! in_array( $field, $field_default, true ) ) {
695 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
696 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
697 1013
698 1014 return;
699 1015 }
700 1016
@@ -728,9 +1044,9 @@
728 1044 $model->save();
729 1045 } elseif ( 'maxmind' === $type ) {
730 1046 try {
731 1047 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
732 - if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
1048 + if ( is_string( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
733 1049 wp_delete_file( $model->geodb_path );
734 1050 }
735 1051 $model->maxmind_license_key = '';
736 1052 $model->geodb_path = '';
@@ -743,13 +1059,39 @@
743 1059 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
744 1060 }
745 1061
746 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 + /**
747 1089 * Unblocks the specified IPs from the firewall.
748 1090 *
749 - * @param string $type The type of data to unblock.
750 - * @param string $field The specific field to unblock.
751 - * @param array $options Command options including IPs to unblock.
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.
752 1094 */
753 1095 private function unblock_firewall( $type, $field, $options ) {
754 1096 $type_default = array( 'ip' );
755 1097 $field_default = array( 'lockout' );
@@ -754,17 +1096,29 @@
754 1096 $type_default = array( 'ip' );
755 1097 $field_default = array( 'lockout' );
756 1098
757 1099 if ( ! in_array( $type, $type_default, true ) ) {
758 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
759 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
760 1108
761 1109 return;
762 1110 }
763 1111
764 1112 if ( ! in_array( $field, $field_default, true ) ) {
765 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
766 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
767 1121
768 1122 return;
769 1123 }
770 1124
@@ -776,10 +1130,16 @@
776 1130 $model->status = Lockout_Ip::STATUS_NORMAL;
777 1131 $model->save();
778 1132 }
779 1133 } else {
780 - WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
781 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
782 1142
783 1143 return;
784 1144 }
785 1145
@@ -789,10 +1149,10 @@
789 1149 /**
790 1150 * Lists details for the firewall based on the specified type and field.
791 1151 * Example: wp defender firewall list user_agent all
792 1152 *
793 - * @param string $type The type of data to list.
794 - * @param string $field The specific field to list.
1153 + * @param string $type The type of data to list.
1154 + * @param string $field The specific field to list.
795 1155 *
796 1156 * @since v2.6.4. Add the details for User Agent Banning.
797 1157 */
798 1158 private function list_firewall( $type, $field ) {
@@ -798,29 +1158,41 @@
798 1158 private function list_firewall( $type, $field ) {
799 1159 $type_default = array( 'user_agent' );
800 1160 $field_default = array( 'all', 'allowlist', 'blocklist' );
801 1161 if ( ! in_array( $type, $type_default, true ) ) {
802 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
803 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
804 1170
805 1171 return;
806 1172 }
807 1173 if ( ! in_array( $field, $field_default, true ) ) {
808 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
809 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
810 1182
811 1183 return;
812 1184 }
813 1185 $model = wd_di()->get( User_Agent_Lockout::class );
814 1186 $data = $model->export();
815 - if ( 'all' === $field && ! empty( $data['whitelist'] ) && ! empty( $data['blacklist'] ) ) {
1187 + if ( 'all' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
816 1188 WP_CLI::log( 'ALLOWLIST:' );
817 1189 WP_CLI::log( $data['whitelist'] );
818 1190 WP_CLI::log( 'BLOCKLIST:' );
819 1191 WP_CLI::log( $data['blacklist'] );
820 - } elseif ( 'allowlist' === $field && ! empty( $data['whitelist'] ) ) {
1192 + } elseif ( 'allowlist' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] ) {
821 1193 WP_CLI::log( $data['whitelist'] );
822 - } elseif ( 'blocklist' === $field && ! empty( $data['blacklist'] ) ) {
1194 + } elseif ( 'blocklist' === $field && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
823 1195 WP_CLI::log( $data['blacklist'] );
824 1196 } else {
825 1197 WP_CLI::log( 'No data.' );
826 1198 }
@@ -830,22 +1202,34 @@
830 1202 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
831 1203 * Example: wp defender firewall activate submodule user_agent
832 1204 * Example: wp defender firewall deactivate submodule login_protection
833 1205 *
834 - * @param string $key_word The keyword to identify the action.
835 - * @param string $submodule The submodule to toggle.
836 - * @param string $action The action to perform (activate or deactivate).
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).
837 1209 */
838 1210 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
839 1211 if ( 'submodule' !== $key_word ) {
840 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
841 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
842 1220
843 1221 return;
844 1222 }
845 1223 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
846 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
847 - WP_CLI::runcommand( 'defender firewall --help' );
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 + );
848 1232
849 1233 return;
850 1234 }
851 1235 // Get submodule slug.
@@ -879,50 +1263,98 @@
879 1263 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
880 1264 }
881 1265
882 1266 /**
883 - * Rename a field to its original model field name.
1267 + * Check if the testing mode is enabled.
1268 + * Outputs an error and returns false if WP_DEFENDER_TESTING is not defined and true.
884 1269 *
885 - * @param string $field The field name to rename.
886 - *
887 - * @return string The renamed field name.
1270 + * @return bool
888 1271 */
889 - private function rename_field( $field ) {
890 - if ( ! empty( $field ) ) {
891 - return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
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;
892 1277 }
893 1278
894 - return '';
1279 + return true;
895 1280 }
896 1281
897 1282 /**
898 - * Check if the specified field is related to country settings.
1283 + * Force Bulk Password Reset.
1284 + * <command>
1285 + * : Action to perform.
1286 + * ---
1287 + * options:
1288 + * - clear
1289 + * ---
899 1290 *
900 - * @param string $field The field to check.
1291 + * ## EXAMPLES
901 1292 *
902 - * @return bool True if the field is related to country settings, false otherwise.
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.
903 1298 */
904 - private function is_country( $field ) {
905 - return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
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 + }
906 1323 }
907 1324
908 1325 /**
909 - * Force Bulk Password Reset.
1326 + * Manage bulk password reset via WP-CLI.
1327 + *
1328 + * ## OPTIONS
1329 + *
910 1330 * <command>
911 - * : Value can be force|undo
912 - * Syntax: wp defender password_reset <command>
913 - * Example: wp defender password_reset force
1331 + * : Action to perform.
1332 + * ---
1333 + * options:
1334 + * - force
1335 + * - undo
1336 + * ---
914 1337 *
915 - * @param mixed $args Command arguments.
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.
916 1348 */
917 1349 public function password_reset( $args ) {
918 1350 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
919 - WP_CLI::log( 'Invalid command.' );
1351 + WP_CLI::error( 'Invalid command.' );
920 1352
921 1353 return;
922 1354 }
923 1355
924 - [ $command ] = $args;
1356 + [$command] = $args;
925 1357 switch ( $command ) {
926 1358 case 'force':
927 1359 // Get the model instance.
928 1360 $model = wd_di()->get( Password_Reset::class );
@@ -928,12 +1360,9 @@
928 1360 $model = wd_di()->get( Password_Reset::class );
929 1361 $model->expire_force = true;
930 1362 $model->force_time = time();
931 1363 $model->save();
932 - $message = sprintf(
933 - 'Passwords created before %s are required to be reset upon next login.',
934 - $this->format_date_time( $model->force_time )
935 - );
1364 + $message = sprintf( 'Passwords created before %s are required to be reset upon next login.', $this->format_date_time( $model->force_time ) );
936 1365 WP_CLI::log( $message );
937 1366 break;
938 1367 case 'undo':
939 1368 $model = wd_di()->get( Password_Reset::class );
@@ -947,36 +1376,42 @@
947 1376 }
948 1377 }
949 1378
950 1379 /**
951 - * Clear completed action scheduler logs.
952 - */
953 - private function scan_clear_logs() {
954 - $scan_component = wd_di()->get( Scan::class );
955 - $result = $scan_component::clear_logs();
956 - $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
957 -
958 - WP_CLI::log( $message );
959 - }
960 -
961 - /**
962 - * Delete old logs.
963 - * <command> delete
964 - * This command must have this command
965 - * Syntax: wp defender logs <command>
966 - * Example: wp defender logs delete
1380 + * Manage Defender's internal log files.
967 1381 *
968 - * @param mixed $args Command arguments.
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.
969 1398 */
970 1399 public function logs( $args ) {
971 1400 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
972 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
973 - WP_CLI::runcommand( 'defender logs --help' );
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 + );
974 1409
975 1410 return;
976 1411 }
977 1412
978 - [ $command ] = $args;
1413 + [$command] = $args;
979 1414
980 1415 switch ( $command ) {
981 1416 case 'delete':
982 1417 $rotation_logger = wd_di()->get( Rotation_Logger::class );
@@ -989,24 +1424,45 @@
989 1424 }
990 1425 }
991 1426
992 1427 /**
993 - * This is a helper for Google Recaptcha actions.
994 - * Syntax: wp defender google_recaptcha <command>
995 - * <command> activate|deactivate|clear
996 - * Example: wp defender google_recaptcha activate
1428 + * Manage CAPTCHA settings via WP-CLI.
997 1429 *
998 - * @param mixed $args Command arguments.
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.
999 1456 */
1000 - public function google_recaptcha( $args ) {
1001 - if ( empty( $args ) ) {
1457 + public function captcha( $args ) {
1458 + if ( ! is_array( $args ) || array() === $args ) {
1002 1459 WP_CLI::error( 'Invalid command.' );
1003 - WP_CLI::runcommand( 'defender google_recaptcha --help' );
1004 1460
1005 1461 return;
1006 1462 }
1007 - $model = wd_di()->get( \WP_Defender\Model\Setting\Recaptcha::class );
1008 - [ $command ] = $args;
1463 + $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1464 + [$command] = $args;
1009 1465 switch ( $command ) {
1010 1466 case 'activate':
1011 1467 if ( true !== $model->enabled ) {
1012 1468 $model->enabled = true;
@@ -1011,9 +1467,9 @@
1011 1467 if ( true !== $model->enabled ) {
1012 1468 $model->enabled = true;
1013 1469 $model->save();
1014 1470 }
1015 - WP_CLI::log( 'Google reCAPTCHA is activated.' );
1471 + WP_CLI::log( 'CAPTCHA is activated.' );
1016 1472 break;
1017 1473 case 'deactivate':
1018 1474 if ( false !== $model->enabled ) {
1019 1475 $model->enabled = false;
@@ -1018,15 +1474,15 @@
1018 1474 if ( false !== $model->enabled ) {
1019 1475 $model->enabled = false;
1020 1476 $model->save();
1021 1477 }
1022 - $model->save();
1023 - WP_CLI::log( 'Google reCAPTCHA is deactivated.' );
1478 + WP_CLI::log( 'CAPTCHA is deactivated.' );
1024 1479 break;
1025 1480 case 'clear':
1026 1481 $default_values = $model->get_default_values();
1027 1482 $model->message = $default_values['message'];
1028 1483 $model->language = 'automatic';
1484 + $model->provider = 'recaptcha';
1029 1485 $model->data_v2_checkbox = array(
1030 1486 'key' => '',
1031 1487 'secret' => '',
1032 1488 'size' => 'normal',
@@ -1040,8 +1496,16 @@
1040 1496 'key' => '',
1041 1497 'secret' => '',
1042 1498 'threshold' => '0.5',
1043 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 + );
1044 1508 $model->locations = array();
1045 1509 $model->detect_woo = false;
1046 1510 $model->woo_checked_locations = array();
1047 1511 $model->detect_buddypress = false;
@@ -1048,13 +1512,12 @@
1048 1512 $model->buddypress_checked_locations = array();
1049 1513 $model->disable_for_known_users = true;
1050 1514 $model->save();
1051 1515
1052 - WP_CLI::log( 'Google reCAPTCHA is cleared.' );
1516 + WP_CLI::log( 'CAPTCHA is cleared.' );
1053 1517 break;
1054 1518 default:
1055 1519 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1056 - WP_CLI::runcommand( 'defender google_recaptcha --help' );
1057 1520 break;
1058 1521 }
1059 1522 }
1060 1523 }