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