PluginProbe ʕ •ᴥ•ʔ
Really Simple Security – Simple and Performant Security (formerly Really Simple SSL) / 9.5.7
Really Simple Security – Simple and Performant Security (formerly Really Simple SSL) v9.5.7
9.5.11 9.5.10.1 9.5.10 trunk 9.4.0 9.4.1 9.4.2 9.4.3 9.5.0 9.5.0.1 9.5.0.2 9.5.1 9.5.2 9.5.2.2 9.5.2.3 9.5.3 9.5.3.1 9.5.3.2 9.5.4 9.5.5 9.5.6 9.5.7 9.5.8 9.5.9
really-simple-ssl / class-wp-cli.php
really-simple-ssl Last commit date
assets 3 months ago core 3 months ago languages 3 months ago lets-encrypt 4 months ago lib 6 months ago mailer 7 months ago modal 3 months ago placeholders 9 months ago progress 1 year ago security 3 months ago settings 3 months ago testssl 5 years ago upgrade 7 months ago .wp-env.json 10 months ago SECURITY.md 9 months ago class-admin.php 3 months ago class-cache.php 4 months ago class-certificate.php 2 years ago class-front-end.php 6 months ago class-installer.php 10 months ago class-mixed-content-fixer.php 3 years ago class-multisite.php 4 months ago class-server.php 4 months ago class-site-health.php 1 year ago class-wp-cli.php 5 months ago compatibility.php 1 year ago force-deactivate.txt 1 year ago functions.php 5 months ago index.php 2 years ago readme.txt 3 months ago rector.php 1 year ago rlrsssl-really-simple-ssl.php 3 months ago rsssl-auto-loader.php 1 year ago sbom.json.gz 3 months ago ssl-test-page.php 2 years ago system-status.php 8 months ago uninstall.php 4 months ago upgrade.php 4 months ago
class-wp-cli.php
1743 lines
1 <?php
2 defined( 'ABSPATH' ) or die();
3
4 require_once rsssl_path . 'lib/admin/class-encryption.php';
5
6 use RSSSL\lib\admin\Encryption;
7 use RSSSL\Pro\Security\WordPress\Firewall\Models\Rsssl_404_Block;
8 use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status;
9 use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository;
10 use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Reminder_Service;
11 use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters;
12
13 /**
14 * WP-CLI integration for Really Simple Security
15 *
16 * For an overview of commands use wp help rsssl
17 *
18 * Usage examples:
19 * wp rsssl activate_ssl
20 * wp rsssl deactivate_ssl
21 * wp rsssl activate_recommended_features
22 * wp rsssl deactivate_recommended_features
23 * wp rsssl activate_security_headers
24 * wp rsssl deactivate_security_headers
25 * wp rsssl update_option --name=site_has_ssl --value=true
26 *
27 * Booleans should be passed to update_option as 0 or 1.
28 *
29 * To complete all standard dashboard notices (recommended features + .htaccess redirect + HSTS + e-mail verification):
30 *
31 * wp rsssl activate_recommended_features
32 * wp rsssl update_option --name=redirect --value=htaccess
33 * wp rsssl update_option --name=hsts --value=1
34 * wp rsssl update_option --name=hsts_preload --value=1
35 * wp rsssl update_option --name=hsts_subdomains --value=1
36 * wp rsssl update_option --name=hsts_max_age --value='63072000'
37 * wp rsssl update_option --name=notifications_email_address --value='you@example.com'
38 * wp option update rsssl_email_verification_status 'completed'
39 */
40 class rsssl_wp_cli {
41
42 use Encryption;
43
44 public function __construct() {
45 if ( $this->wp_cli_active() ) {
46 add_action( 'init', [ $this, 'register_wp_cli_commands' ], 0 );
47 }
48 }
49
50 /**
51 * Checks if the conditions for running a Pro WP-CLI command are met.
52 * This is called *within* the command handler, ensuring plugin is loaded.
53 * Outputs an error and exits if conditions are not met.
54 *
55 * @return bool True if conditions are met, false otherwise (though it usually exits on false).
56 */
57 private function check_pro_command_preconditions(bool $skip_license = false ): bool {
58 // Skip license check for free (non-pro) commands
59 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
60 $command = $backtrace[1]['function'] ?? '';
61 $command_list = $this->get_command_list();
62 if ( isset($command_list[$command]) && $command_list[$command]['pro'] === false ) {
63 return true;
64 }
65 // Check if Pro is active (redundant check, but safe)
66 if ( ! defined( 'rsssl_pro' ) ) {
67 WP_CLI::error(
68 __( 'This command is related to functionality available in Really Simple Security Pro, please consider upgrading to unlock all powerful security features. Read more: https://really-simple-ssl.com/pro', 'really-simple-ssl' ),
69 true // Exit after error
70 );
71 return false; // Should not be reached
72 }
73
74 if ( $skip_license ) {
75 return true; // Skip license check if explicitly requested
76 }
77 // Check if license is valid (now safe to call)
78 if ( ! RSSSL()->licensing->license_is_valid() ) {
79 $activate_command = 'wp rsssl activate_license <YOUR_LICENSE_KEY>';
80 // Check if the command exists in the list just to be safe
81 if (!isset($this->get_command_list()['activate_license'])) {
82 $activate_command = 'activate_license'; // Fallback text
83 }
84 WP_CLI::error(
85 sprintf(
86 __( 'It seems that no valid license key is activated for this domain. Activate your license key using the `%s` command, or purchase a valid license key via https://really-simple-ssl.com/pro', 'really-simple-ssl' ),
87 $activate_command
88 ),
89 true // Exit after error
90 );
91 return false; // Should not be reached
92 }
93
94 // All checks passed
95 return true;
96 }
97
98 /**
99 * Check if WP-CLI is active.
100 *
101 * @return bool True if WP-CLI is active, false otherwise.
102 */
103 public function wp_cli_active() {
104 return defined( 'WP_CLI' ) && WP_CLI;
105 }
106
107 /**
108 * Activate SSL through WP-CLI.
109 *
110 * Provides options for verbose output, forcing activation despite warnings,
111 * skipping confirmation, and performing a dry run.
112 *
113 * ## OPTIONS
114 *
115 * [--verbose]
116 * : Show detailed steps during activation.
117 *
118 * [--force]
119 * : Force activation even if pre-flight checks issue warnings and skip confirmation prompt.
120 *
121 * [--yes]
122 * : Skip the confirmation prompt before activating.
123 *
124 * [--dry-run]
125 * : Perform checks and report intended actions without making changes.
126 *
127 * ## EXAMPLES
128 *
129 * wp rsssl activate_ssl
130 * wp rsssl activate_ssl --verbose --yes
131 * wp rsssl activate_ssl --dry-run
132 *
133 * @param array $args Positional arguments (none used here).
134 * @param array $assoc_args Associative arguments (--verbose, --force, --yes, --dry-run).
135 * @return void
136 */
137 public function activate_ssl( $args, $assoc_args ) {
138 if ( ! $this->check_pro_command_preconditions() ) return;
139 $is_verbose = WP_CLI\Utils\get_flag_value( $assoc_args, 'verbose', false );
140 $is_force = WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
141 $skip_confirm = WP_CLI\Utils\get_flag_value( $assoc_args, 'yes', false );
142 $is_dry_run = WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
143
144 if ( $is_dry_run ) {
145 WP_CLI::line( "-- Dry Run Enabled: No changes will be made. --" );
146 }
147
148 try {
149 // --- Suggestion 3: Pre-flight Checks ---
150 if ( $is_verbose || $is_dry_run ) WP_CLI::debug( 'Running pre-activation checks...', 'rsssl-cli' );
151
152 // Assume this function now exists and returns ['success' => bool, 'message' => string, 'warnings' => array]
153 $checks = $this->perform_pre_flight_checks();
154
155 if ( ! empty( $checks['warnings'] ) ) {
156 foreach ( $checks['warnings'] as $warning ) {
157 WP_CLI::warning( $warning );
158 }
159 if ( ! $is_force && ! $is_dry_run ) {
160 WP_CLI::error( 'Pre-flight checks issued warnings. Use --force to proceed anyway.', false ); // Use false to allow dry-run continue
161 if (!$is_dry_run) return; // Stop if not dry run
162 }
163 }
164
165 if ( ! $checks['success'] ) {
166 // If checks outright fail (not just warnings)
167 WP_CLI::error( 'Pre-flight checks failed: ' . $checks['message'] );
168 return;
169 }
170
171 if ( $is_verbose || $is_dry_run ) WP_CLI::debug( 'Pre-flight checks passed.', 'rsssl-cli' );
172
173
174 // --- Report Intended Actions (Dry Run) ---
175 if ( $is_dry_run ) {
176 WP_CLI::line( "Intended actions:" );
177 WP_CLI::line( "- Update WordPress Site URL and Home URL to HTTPS." );
178 WP_CLI::line( "- Configure redirects (method depends on settings)." );
179 WP_CLI::line( "- Update internal links/content (if mixed content fixer enabled)." );
180 WP_CLI::line( "- Dismiss onboarding notice." );
181 WP_CLI::success( "Dry run complete. No changes were made." );
182 return; // End dry run here
183 }
184
185
186 // --- Suggestion 4: Confirmation Prompt ---
187 // Skip confirmation if --yes or --force is used
188 if ( ! $skip_confirm && ! $is_force ) {
189 WP_CLI::confirm( 'Are you sure you want to activate SSL for this site?' );
190 // WP_CLI::confirm exits script if user doesn't confirm
191 }
192
193 // --- Core Activation Logic ---
194 if ( $is_verbose ) WP_CLI::debug( 'Attempting SSL activation...', 'rsssl-cli' );
195
196 // --- Suggestion 5: Clarify Side Effects ---
197 // Move onboarding dismissal inside the main activation logic or make it explicit
198 // update_option( 'rsssl_onboarding_dismissed', true, false ); // Optionally moved inside activate_ssl or reported
199
200 // --- Suggestion 1: Granular Failure Reasons ---
201 // Assume RSSSL()->admin->activate_ssl() now returns an array or throws specific exceptions
202 // Passing $is_verbose allows the underlying function to potentially output debug info too
203 $result = RSSSL()->admin->activate_ssl( $is_verbose );
204
205 // Check if $result is structured like ['success' => bool, 'message' => string]
206 if ( is_array( $result ) && isset( $result['success'] ) ) {
207 if ( $result['success'] ) {
208 $success_message = 'SSL activated successfully.';
209 // Suggestion 5: Clarify Side Effects (Example)
210 if ( get_option('rsssl_onboarding_dismissed') ) {
211 $success_message .= ' Onboarding notice dismissed.';
212 }
213 WP_CLI::success( $success_message );
214 } else {
215 // Use the detailed message from the function
216 WP_CLI::error( 'SSL activation failed: ' . ( $result['message'] ?? 'Unknown reason.' ) );
217 }
218 } else if ( $result === true ) { // Handle simple boolean success
219 WP_CLI::success( 'SSL activated successfully. Onboarding notice dismissed.' );
220 } else { // Handle simple boolean failure or unexpected return
221 WP_CLI::error( 'SSL activation failed (unknown reason).' );
222 }
223
224
225 } catch ( Exception $e ) { // Catch specific exceptions if activate_ssl throws them
226 // Suggestion 1 & 2: More specific error based on exception type if possible
227 WP_CLI::error( 'Failed to activate SSL due to an unexpected error: ' . $e->getMessage() );
228 }
229 }
230
231 /**
232 * Deactivate SSL through WP-CLI.
233 *
234 * @return void
235 */
236 public function deactivate_ssl() {
237 if ( ! $this->check_pro_command_preconditions() ) return;
238 try {
239 RSSSL()->admin->deactivate();
240 WP_CLI::success( 'SSL deactivated' );
241 } catch ( Exception $e ) {
242 WP_CLI::error( 'Failed to deactivate SSL: ' . $e->getMessage() );
243 }
244 }
245
246 /**
247 * Update a Really Simple Security option via WP-CLI.
248 * Booleans should be passed as 0 or 1.
249 *
250 * @param array $args Command-line positional arguments.
251 * @param array $assoc_args Command-line associative arguments.
252 *
253 * @return void
254 */
255 public function update_option( $args, $assoc_args ) {
256 if ( ! isset( $assoc_args['name'] ) || ! isset( $assoc_args['value'] ) ) {
257 WP_CLI::error( 'Both --name and --value parameters are required.' );
258 }
259
260 $name = sanitize_title( $assoc_args['name'] );
261 $value = $assoc_args['value'];
262
263 try {
264 rsssl_update_option( $name, $value );
265 WP_CLI::success( "Option $name updated to $value" );
266 } catch ( Exception $e ) {
267 WP_CLI::error( 'Failed to update option: ' . $e->getMessage() );
268 }
269 }
270
271 /**
272 * Activate all recommended features via CLI
273 *
274 * @throws Exception
275 * return void
276 */
277 public function activate_recommended_features() {
278 if ( ! $this->check_pro_command_preconditions() ) return;
279 try {
280 RSSSL()->admin->activate_recommended_features();
281 } catch ( Exception $e ) {
282 WP_CLI::error( 'Failed to activate recommended features. ' . $e->getMessage() );
283 }
284
285 WP_CLI::success( 'Recommended features activated.' );
286 }
287
288 /**
289 * Deactivate all recommended features via CLI
290 *
291 * return void
292 */
293 public function deactivate_recommended_features() {
294 if ( ! $this->check_pro_command_preconditions() ) return;
295 try {
296 // Deactivate Vulnerability Scanner
297 rsssl_update_option( 'enable_vulnerability_scanner', false );
298
299 // Deactivate essential WordPress hardening features
300 if (isset(RSSSL()->settingsConfigService)) {
301 $recommended_hardening_fields = RSSSL()->settingsConfigService->getRecommendedHardeningSettings();
302 foreach ( $recommended_hardening_fields as $field ) {
303 rsssl_update_option( $field, false );
304 }
305 }
306
307 // Disable Email login protection
308 rsssl_update_option( 'login_protection_enabled', false );
309
310 // Disable Mixed Content Fixer
311 rsssl_update_option( 'mixed_content_fixer', false );
312
313 // Disable firewall
314 rsssl_update_option( 'enable_firewall', false );
315 rsssl_update_option( 'event_log_enabled', false );
316 // Check if PRO version is active, then deactivate premium features
317 if ( defined( 'rsssl_pro' ) ) {
318 // Disable Two-Factor Authentication
319 rsssl_update_option( 'two_fa_enabled_roles_totp', [] );
320
321 // Disable Limit Login Attempts
322 rsssl_update_option( 'enable_limited_login_attempts', false );
323
324 // Disable advanced security headers
325 $security_headers = [
326 'upgrade_insecure_requests',
327 'x_content_type_options',
328 'hsts',
329 'x_xss_protection',
330 'x_frame_options',
331 'referrer_policy',
332 'csp_frame_ancestors',
333 ];
334 foreach ( $security_headers as $header_key => $header_value ) {
335 if ( is_string( $header_key ) ) {
336 rsssl_update_option( $header_key, false );
337 } else {
338 rsssl_update_option( $header_value, false );
339 }
340 }
341
342 // Deactivate password security enforcement
343 rsssl_update_option( 'enforce_password_security_enabled', false );
344 rsssl_update_option( 'enable_hibp_check', false );
345 }
346
347 do_action('rsssl_update_rules');
348 WP_CLI::success( 'Recommended features deactivated.' );
349 } catch ( Exception $e ) {
350 WP_CLI::error( 'Failed to deactivate recommended features: ' . $e->getMessage() );
351 }
352 }
353
354 /**
355 * Activate all recommended hardening features via CLI
356 *
357 * return void
358 */
359 public function activate_recommended_hardening_features() {
360 if ( ! $this->check_pro_command_preconditions() ) return;
361 try {
362 if (isset(RSSSL()->settingsConfigService)) {
363 $recommended_hardening_fields = RSSSL()->settingsConfigService->getRecommendedHardeningSettings();
364 foreach ( $recommended_hardening_fields as $field ) {
365 rsssl_update_option( $field, true );
366 }
367 }
368 do_action('rsssl_update_rules');
369 WP_CLI::success( 'Recommended hardening features activated.' );
370 } catch ( Exception $e ) {
371 WP_CLI::error( 'Failed to activate recommended hardening features: ' . $e->getMessage() );
372 }
373 }
374
375 /**
376 * Deactivate all recommended features via CLI
377 *
378 * return void
379 */
380 public function deactivate_recommended_hardening_features() {
381 if ( ! $this->check_pro_command_preconditions() ) return;
382 try {
383 if (isset(RSSSL()->settingsConfigService)) {
384 $recommended_hardening_fields = RSSSL()->settingsConfigService->getRecommendedHardeningSettings();
385 foreach ( $recommended_hardening_fields as $field ) {
386 rsssl_update_option( $field, false );
387 }
388 }
389 do_action('rsssl_update_rules');
390 WP_CLI::success( 'Recommended hardening features deactivated.' );
391 } catch ( Exception $e ) {
392 WP_CLI::error( 'Failed to deactivate recommended hardening features: ' . $e->getMessage() );
393 }
394 }
395
396
397 /**
398 * Activate recommended security headers via CLI
399 */
400 public function activate_security_headers() {
401 if ( ! $this->check_pro_command_preconditions() ) return;
402 try {
403 foreach (RSSSL()->headers->get_recommended_security_headers() as $header ) {
404 if (isset($header['option_name'], $header['recommended_setting'])) {
405 rsssl_update_option( $header['option_name'], $header['recommended_setting'] );
406 }
407 }
408 WP_CLI::success( 'Recommended security header settings saved. Run "update_advanced_headers" command to activate them.' );
409 do_action('rsssl_update_rules');
410 } catch ( Exception $e ) {
411 WP_CLI::error( 'Failed to activate security headers: ' . $e->getMessage() );
412 }
413 }
414
415
416 /**
417 * Deactivate recommended security headers via CLI
418 */
419 public function deactivate_security_headers() {
420 if ( ! $this->check_pro_command_preconditions() ) return;
421 try {
422 $recommended_headers = RSSSL()->headers->get_recommended_security_headers();
423
424 foreach ( $recommended_headers as $header ) {
425 if ( isset( $header['option_name'] ) && isset( $header['disabled_setting'] ) ) {
426 rsssl_update_option($header['option_name'], $header['disabled_setting']);
427 }
428 }
429 do_action('rsssl_update_rules');
430 WP_CLI::success( 'Recommended security headers deactivated.' );
431 } catch ( Exception $e ) {
432 WP_CLI::error( 'Failed to deactivate security headers: ' . $e->getMessage() );
433 }
434 }
435
436 /**
437 * Activate firewall via CLI
438 *
439 * return void
440 */
441
442 public function activate_firewall() {
443 if ( ! $this->check_pro_command_preconditions() ) return;
444 try {
445 rsssl_update_option( 'enable_firewall', true );
446 rsssl_update_option( 'event_log_enabled', true );
447 do_action('rsssl_update_rules');
448 WP_CLI::success( 'Firewall activated.' );
449 } catch ( Exception $e ) {
450 WP_CLI::error( 'Failed to activate firewall: ' . $e->getMessage() );
451 }
452 }
453
454 /**
455 * Deactivate firewall via CLI
456 *
457 * return void
458 */
459 public function deactivate_firewall() {
460 if ( ! $this->check_pro_command_preconditions() ) return;
461 try {
462 rsssl_update_option( 'enable_firewall', false );
463 rsssl_update_option( 'event_log_enabled', false );
464 do_action('rsssl_update_rules');
465 WP_CLI::success( 'Firewall deactivated.' );
466 } catch ( Exception $e ) {
467 WP_CLI::error( 'Failed to deactivate firewall: ' . $e->getMessage() );
468 }
469 }
470
471 /**
472 * Activate Two-Factor Authentication via CLI
473 *
474 * return void
475 */
476 public function activate_2fa() {
477 if ( ! $this->check_pro_command_preconditions() ) return;
478 try {
479 rsssl_update_option( 'two_fa_enabled_roles_totp', [ 'administrator' ] );
480 rsssl_update_option( 'login_protection_enabled', true );
481 WP_CLI::success( 'Two-Factor Authentication activated.' );
482 } catch ( Exception $e ) {
483 WP_CLI::error( 'Failed to activate Two-Factor Authentication: ' . $e->getMessage() );
484 }
485 }
486
487 /**
488 * Deactivate Two-Factor Authentication via CLI
489 *
490 * return void
491 */
492 public function deactivate_2fa() {
493 if ( ! $this->check_pro_command_preconditions() ) return;
494 try {
495 rsssl_update_option( 'two_fa_enabled_roles_totp', [] );
496 rsssl_update_option( 'login_protection_enabled', false );
497 WP_CLI::success( 'Two-Factor Authentication deactivated.' );
498 } catch ( Exception $e ) {
499 WP_CLI::error( 'Failed to deactivate Two-Factor Authentication: ' . $e->getMessage() );
500 }
501 }
502
503 /**
504 * Activate password security via CLI
505 *
506 * return void
507 */
508 public function activate_password_security() {
509 if ( ! $this->check_pro_command_preconditions() ) return;
510 try {
511 rsssl_update_option( 'enforce_password_security_enabled', true );
512 rsssl_update_option( 'enforce_frequent_password_change', true );
513 rsssl_update_option( 'hide_rememberme', true );
514 rsssl_update_option( 'enable_hibp_check', true );
515 WP_CLI::success( 'Password security features activated.' );
516 } catch ( Exception $e ) {
517 WP_CLI::error( 'Failed to activate password security: ' . $e->getMessage() );
518 }
519 }
520
521 /**
522 * Deactivate password security via CLI
523 *
524 * return void
525 */
526 public function deactivate_password_security() {
527 if ( ! $this->check_pro_command_preconditions() ) return;
528 try {
529 rsssl_update_option( 'enforce_password_security_enabled', false );
530 rsssl_update_option( 'enforce_frequent_password_change', false );
531 rsssl_update_option( 'hide_rememberme', false );
532 rsssl_update_option( 'enable_hibp_check', false );
533 do_action('rsssl_update_rules');
534 WP_CLI::success( 'Password security features deactivated.' );
535 } catch ( Exception $e ) {
536 WP_CLI::error( 'Failed to deactivate password security: ' . $e->getMessage() );
537 }
538 }
539
540 /**
541 * Activate login attempts limitation via CLI
542 *
543 * return void
544 */
545 public function activate_lla() {
546 if ( ! $this->check_pro_command_preconditions() ) return;
547 try {
548 rsssl_update_option( 'enable_limited_login_attempts', true );
549 rsssl_update_option( 'event_log_enabled', true );
550 WP_CLI::success( 'Limit login attempts activated.' );
551 do_action('rsssl_update_rules');
552 } catch ( Exception $e ) {
553 WP_CLI::error( 'Failed to activate limit login attempts: ' . $e->getMessage() );
554 }
555 }
556
557 /**
558 * Deactivate login attempts limitation via CLI
559 *
560 * return void
561 */
562 public function deactivate_lla() {
563 if ( ! $this->check_pro_command_preconditions() ) return;
564 try {
565 rsssl_update_option( 'enable_limited_login_attempts', false );
566 rsssl_update_option( 'event_log_enabled', false );
567 do_action('rsssl_update_rules');
568 WP_CLI::success( 'Limit login attempts deactivated.' );
569 } catch ( Exception $e ) {
570 WP_CLI::error( 'Failed to deactivate limit login attempts: ' . $e->getMessage() );
571 }
572 }
573
574 /**
575 * Activate vulnerability scanning via CLI
576 *
577 * return void
578 */
579 public function activate_vulnerability_scanning() {
580 if ( ! $this->check_pro_command_preconditions() ) return;
581 try {
582 rsssl_update_option( 'enable_vulnerability_scanner', true );
583
584 WP_CLI::success( 'Vulnerability scanning activated.' );
585 } catch ( Exception $e ) {
586 WP_CLI::error( 'Failed to activate vulnerability scanning: ' . $e->getMessage() );
587 }
588 }
589
590 /**
591 * Deactivate vulnerability scanning via CLI
592 *
593 * return void
594 */
595 public function deactivate_vulnerability_scanning() {
596 if ( ! $this->check_pro_command_preconditions() ) return;
597 try {
598 rsssl_update_option( 'enable_vulnerability_scanner', false );
599
600 WP_CLI::success( 'Vulnerability scanning deactivated.' );
601 } catch ( Exception $e ) {
602 WP_CLI::error( 'Failed to deactivate vulnerability scanning: ' . $e->getMessage() );
603 }
604 }
605
606 /**
607 * Activate license via CLI
608 *
609 * @param array $args Positional arguments. License should be passed as first and only argument
610 *
611 * @return void
612 */
613 public function activate_license( $args ) {
614 if ( ! $this->check_pro_command_preconditions(true) ) return;
615 try {
616 // Check if license key is provided
617 if ( empty( $args[0] ) ) {
618 WP_CLI::error( 'Please provide a license key: wp rsssl activate_license YOUR_LICENSE_KEY' );
619
620 return;
621 }
622
623 $license_key = sanitize_text_field( $args[0] );
624
625 rsssl_update_option( 'license', $this->encrypt_with_prefix( $license_key, 'really_simple_ssl_' ) );
626 $status = RSSSL()->licensing->get_license_status( 'check_license', true );
627
628 update_option( 'rsssl_onboarding_dismissed', true, false );
629
630 if ( $status === 'valid' ) {
631 WP_CLI::success( 'License activated successfully.' );
632 } elseif ( $status === 'invalid' || $status === 'missing' ) {
633 WP_CLI::error( 'Invalid license key. You can find your license key on https://really-simple-ssl.com/account' );
634 } elseif ( $status === 'expired' ) {
635 WP_CLI::error( 'License has expired. Please renew via https://really-simple-ssl.com/account/subscriptions' );
636 } elseif ( $status === 'no_activations_left' ) {
637 WP_CLI::error( 'No activations left. Please upgrade your license via https://really-simple-ssl.com/account/subscriptions' );
638 } elseif ( $status === 'disabled' ) {
639 WP_CLI::error( 'This license is not valid. Find out why on your account page at https://really-simple-ssl.com/account' );
640 }
641 } catch ( Exception $e ) {
642 WP_CLI::error( 'Failed to activate license: ' . $e->getMessage() );
643 }
644 }
645
646 /**
647 * Deactivate license via CLI
648 *
649 * @return void
650 */
651 public function deactivate_license() {
652 if ( ! $this->check_pro_command_preconditions() ) return;
653 try {
654 rsssl_update_option( 'license', '' );
655 $status = RSSSL()->licensing->get_license_status( 'check_license', true );
656 update_option( 'rsssl_onboarding_dismissed', true, false );
657
658 // License key should now be empty
659 if ( $status === 'empty' ) {
660 WP_CLI::success( 'License deactivated successfully.' );
661 } else {
662 WP_CLI::error( 'Something went wrong when deactivating your license. Please try again.' );
663 }
664
665 } catch ( Exception $e ) {
666 WP_CLI::error( 'Failed to deactivate license: ' . $e->getMessage() );
667 }
668 }
669
670 /**
671 * Add lock file for safe mode
672 *
673 * @return void
674 */
675 public function add_lock_file() {
676 if ( ! $this->check_pro_command_preconditions() ) return;
677 try {
678 $lock_file = WP_CONTENT_DIR . '/rsssl-safe-mode.lock';
679
680 // Check if file already exists
681 if ( file_exists( $lock_file ) ) {
682 WP_CLI::warning( 'Lock file already exists.' );
683
684 return;
685 }
686
687 // Create lock file
688 $result = file_put_contents( $lock_file, time() );
689
690 if ( $result === false ) {
691 WP_CLI::error( 'Unable to create lock file.' );
692 }
693
694 // Set proper permissions
695 chmod( $lock_file, 0644 );
696
697 WP_CLI::success( 'Safe mode lock file created successfully.' );
698 } catch ( Exception $e ) {
699 WP_CLI::error( 'Failed to create lock file: ' . $e->getMessage() );
700 }
701 }
702
703 /**
704 * Remove lock file for safe mode
705 *
706 * @return void
707 */
708 public function remove_lock_file() {
709 if ( ! $this->check_pro_command_preconditions() ) return;
710 try {
711 $lock_file = WP_CONTENT_DIR . '/rsssl-safe-mode.lock';
712
713 // Check if file exists
714 if ( ! file_exists( $lock_file ) ) {
715 WP_CLI::warning( 'Lock file does not exist.' );
716
717 return;
718 }
719
720 // Remove lock file
721 if ( ! unlink( $lock_file ) ) {
722 WP_CLI::error( 'Unable to remove lock file.' );
723 }
724
725 WP_CLI::success( 'Safe mode lock file removed successfully.' );
726 } catch ( Exception $e ) {
727 WP_CLI::error( 'Failed to remove lock file: ' . $e->getMessage() );
728 }
729 }
730
731 /**
732 * Reset the 2FA status of a user to disabled
733 *
734 * Usage: wp rsssl reset_2fa 123
735 *
736 * @param array $args User ID should be the first element
737 *
738 * @throws \WP_CLI\ExitException
739 */
740 public function reset_2fa( $args ): void
741 {
742 if ( ! $this->check_pro_command_preconditions() ) return;
743 // When empty array is passed, WP_CLI will return an error
744 if ( empty( $args ) ) {
745 WP_CLI::error( 'Please provide a user ID.', true );
746 }
747 $user_id = intval( $args[0] );
748 $user = get_user_by('id', $user_id);
749
750 if (empty($user)) {
751 WP_CLI::error('User not found.', true);
752 }
753
754 if (!class_exists('Rsssl_Two_Fa_Status')) {
755 require_once rsssl_path . '/security/wordpress/two-fa/class-rsssl-two-fa-status.php';
756 }
757
758 if ( $user ) {
759 // Delete all 2fa related user meta.
760 Rsssl_Two_Fa_Status::delete_two_fa_meta( $user->ID );
761 // Set the last login to now, so the user will be forced to use 2fa.
762 update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) );
763 delete_user_meta( $user->ID, 'rsssl_passkey_configured'); // Remove passkey configuration if it exists
764 }
765
766 WP_CLI::success( 'Successfully reset 2FA for user id ' . $user_id );
767 }
768
769 /**
770 * Preview (dry-run) which users are in scope for 2FA reminders, optionally across subsites.
771 *
772 * Usage examples:
773 * wp rsssl twofa_preview
774 * wp rsssl twofa_preview --role=editor
775 * wp rsssl twofa_preview --include-subsites
776 * wp rsssl twofa_preview --site=7 --format=json
777 * wp rsssl twofa_preview --reset-meta
778 */
779 public function twofa_preview( $args, $assoc_args ) {
780 if ( ! $this->check_pro_command_preconditions() ) return;
781
782 $role = $assoc_args['role'] ?? 'all';
783 $format = $assoc_args['format'] ?? 'table';
784 $includeNetwork = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-subsites', false );
785 $specificSiteId = $assoc_args['site'] ?? null;
786 $doResetMeta = \WP_CLI\Utils\get_flag_value( $assoc_args, 'reset-meta', false );
787
788 $rows = $this->collect_twofa_rows( $role, $includeNetwork, $specificSiteId, $doResetMeta );
789
790 if ( empty( $rows ) ) {
791 \WP_CLI::success( 'Geen gebruikers gevonden in de huidige 2FA scope.' );
792 return;
793 }
794
795 \WP_CLI\Utils\format_items( $format, $rows, [ 'blog_id','user_id','user_login','email','roles','reminder_sent' ] );
796 }
797
798 /**
799 * Send 2FA reminders for the current selection. Explicitly triggers the send flow per (sub)site.
800 *
801 * Usage examples:
802 * wp rsssl twofa_send
803 * wp rsssl twofa_send --role=author --site=3
804 * wp rsssl twofa_send --include-subsites --reset-meta
805 */
806 public function twofa_send( $args, $assoc_args ) {
807 if ( ! $this->check_pro_command_preconditions() ) return;
808
809 $role = $assoc_args['role'] ?? 'all';
810 $includeNetwork = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-subsites', false );
811 $specificSiteId = $assoc_args['site'] ?? null;
812 $doResetMeta = \WP_CLI\Utils\get_flag_value( $assoc_args, 'reset-meta', false );
813
814 $service = new Rsssl_Two_Fa_Reminder_Service();
815 $siteIds = $this->determine_sites_for_twofa( $includeNetwork, $specificSiteId );
816 $total = 0;
817
818 foreach ( $siteIds as $blog_id ) {
819 $this->with_blog_for_twofa( (int) $blog_id, function() use ( $role, $service, $doResetMeta, &$total, $blog_id ) {
820 $repo = new Rsssl_Two_Fa_User_Repository();
821 $params = new Rsssl_Two_FA_Data_Parameters([
822 'filter_column' => 'user_role',
823 'filter_value' => $role,
824 ]);
825 $collection = $repo->getForcedTwoFaUsersWithOpenStatus( $params );
826
827 if ( $doResetMeta ) {
828 foreach ( $collection->getUsers() as $u ) {
829 delete_user_meta( $u->getId(), 'rsssl_two_fa_reminder_sent' );
830 }
831 }
832
833 $countBefore = (int) $collection->getTotalRecords();
834 if ( $countBefore > 0 ) {
835 \WP_CLI::log( sprintf( 'Blog %d: verstuur reminders naar %d gebruiker(s)...', (int) $blog_id, $countBefore ) );
836 $service->processReminders( $collection );
837 $total += $countBefore;
838 } else {
839 \WP_CLI::log( sprintf( 'Blog %d: geen kandidaten.', (int) $blog_id ) );
840 }
841 } );
842 }
843
844 \WP_CLI::success( sprintf( 'Verzenden gereed. Totaal verstuurd: %d', (int) $total ) );
845 }
846
847 /** ----------------- Helpers (private) ----------------- */
848
849 /**
850 * Build preview rows for users in scope.
851 */
852 private function collect_twofa_rows( string $role, bool $includeNetwork, $specificSiteId, bool $doResetMeta ): array {
853 $rows = [];
854 $siteIds = $this->determine_sites_for_twofa( $includeNetwork, $specificSiteId );
855
856 foreach ( $siteIds as $blog_id ) {
857 $this->with_blog_for_twofa( (int) $blog_id, function() use ( $role, $doResetMeta, $blog_id, &$rows ) {
858 $repo = new Rsssl_Two_Fa_User_Repository();
859 $params = new Rsssl_Two_FA_Data_Parameters([
860 'filter_column' => 'user_role',
861 'filter_value' => $role,
862 ]);
863
864 foreach ( $repo->getForcedTwoFaUsersWithOpenStatus( $params )->getUsers() as $u ) {
865 $user_id = (int) $u->getId();
866 $wp_user = get_userdata( $user_id );
867 if ( ! $wp_user ) {
868 continue;
869 }
870
871 if ( $doResetMeta ) {
872 delete_user_meta( $user_id, 'rsssl_two_fa_reminder_sent' );
873 }
874
875 $rows[] = [
876 'blog_id' => (string) $blog_id,
877 'user_id' => (string) $user_id,
878 'user_login' => $wp_user->user_login,
879 'email' => $wp_user->user_email,
880 'roles' => implode( ',', $wp_user->roles ?? [] ),
881 'reminder_sent' => get_user_meta( $user_id, 'rsssl_two_fa_reminder_sent', true ) ? 'yes' : 'no',
882 ];
883 }
884 } );
885 }
886
887 return $rows;
888 }
889
890 /**
891 * Decide which sites to traverse for multisite support.
892 */
893 private function determine_sites_for_twofa( bool $includeNetwork, $specificSiteId ): array {
894 if ( is_multisite() ) {
895 if ( ! empty( $specificSiteId ) ) {
896 return [ (int) $specificSiteId ];
897 }
898 if ( $includeNetwork ) {
899 $ids = [];
900 foreach ( get_sites( [ 'fields' => 'ids', 'number' => 0 ] ) as $bid ) {
901 $ids[] = (int) $bid;
902 }
903 return $ids;
904 }
905 return [ get_current_blog_id() ];
906 }
907 return [ 0 ];
908 }
909
910 /**
911 * Execute a callback within the context of a (sub)site.
912 */
913 private function with_blog_for_twofa( int $blog_id, callable $cb ): void {
914 if ( is_multisite() && $blog_id > 0 ) {
915 switch_to_blog( $blog_id );
916 try {
917 $cb();
918 } finally {
919 restore_current_blog();
920 }
921 } else {
922 $cb();
923 }
924 }
925
926 /**
927 * Update the advanced-headers.php with the latest rules
928 *
929 * @return void
930 */
931 public function update_advanced_headers() {
932 if ( ! $this->check_pro_command_preconditions() ) return;
933 do_action('rsssl_update_rules');
934 WP_CLI::success( 'Successfully update advanced headers.' );
935 }
936
937 /**
938 * Add an IP to the firewall blocklist.
939 *
940 * @example wp rsssl add_firewall_ip_block 123.123.123.1 --note="This is a temporary block"
941 * @example wp rsssl add_firewall_ip_block 123.123.123.1 --permanent --note="This is a permanent block"
942 *
943 * @param array $args Should contain IP as the first element
944 * @param array $assoc_args Can contain a note with a 'note' key
945 */
946 public function add_firewall_ip_block(array $args, array $assoc_args): void
947 {
948 if ( ! $this->check_pro_command_preconditions() ) return;
949 $this->handleFirewallTableEntry($args, $assoc_args, 'blocked', 'add');
950 }
951
952 /**
953 * Can be used to remove a (temporary) block from the firewall blocklist.
954 * @example wp rsssl remove_firewall_ip_block 123.123.123.1
955 *
956 * @param $args array Should contain the ip address
957 */
958 public function remove_firewall_ip_block(array $args, array $assoc_args ): void
959 {
960 if ( ! $this->check_pro_command_preconditions() ) return;
961 $this->handleFirewallTableEntry($args, $assoc_args, 'blocked', 'remove');
962 }
963
964 /**
965 * Return a table of the current blocked IPs with the headers:
966 * IP Address, Note, Permanent
967 */
968 public function show_blocked_ips() {
969 if ( ! $this->check_pro_command_preconditions() ) return;
970 $columns = [
971 'ip_address',
972 'note',
973 'permanent',
974 ];
975
976 $blockedIps = ( new Rsssl_404_Block() )->get_blocked_ips($columns);
977
978 WP_CLI\Utils\format_items('table', $blockedIps, $columns);
979 }
980
981 /**
982 * Add an IP to the firewall's trusted list.
983 *
984 * Usage: wp rsssl add_firewall_trusted_ip 123.123.123.1
985 *
986 * @param array $args Should contain IP as the first element
987 * @param array $assoc_args Can contain a note with a 'note' key
988 * @uses handleFirewallTableEntry()
989 */
990 public function add_firewall_trusted_ip(array $args, array $assoc_args) {
991 if ( ! $this->check_pro_command_preconditions() ) return;
992 $this->handleFirewallTableEntry($args, $assoc_args, 'trusted', 'add');
993 }
994
995 /**
996 * Remove an IP from the firewall's trusted list.
997 *
998 * Usage: wp rsssl remove_firewall_trusted_ip 123.123.123.1
999 *
1000 * @param array $args Should contain IP as the first element
1001 * @param array $assoc_args Can contain a note with a 'note' key
1002 * @uses handleFirewallTableEntry()
1003 */
1004 public function remove_firewall_trusted_ip(array $args, array $assoc_args) {
1005 if ( ! $this->check_pro_command_preconditions() ) return;
1006 $this->handleFirewallTableEntry($args, $assoc_args, 'trusted', 'remove');
1007 }
1008
1009 /**
1010 * Add an IP to the LLA's trusted list.
1011 *
1012 * Usage: wp rsssl add_lla_trusted_ip 123.123.123.1
1013 *
1014 * @param array $args Command arguments.
1015 * @uses handleLlaTableEntry()
1016 */
1017 public function add_lla_trusted_ip( $args ) {
1018 if ( ! $this->check_pro_command_preconditions() ) return;
1019 $this->handleLlaTableEntry($args, 'allowed', 'source_ip', 'add');
1020 }
1021
1022 /**
1023 * Add an IP to the LLA's blocklist.
1024 *
1025 * Usage: wp rsssl remove_lla_trusted_ip 123.123.123.1
1026 *
1027 * @param array $args Command arguments.
1028 * @uses handleLlaTableEntry()
1029 */
1030 public function remove_lla_trusted_ip( $args ) {
1031 if ( ! $this->check_pro_command_preconditions() ) return;
1032 $this->handleLlaTableEntry($args, 'allowed', 'source_ip', 'remove');
1033 }
1034
1035 /**
1036 * Remove an IP from the LLA's trusted list.
1037 *
1038 * Usage: wp rsssl add_lla_blocked_ip 123.123.123.1
1039 * Usage: wp rsssl add_lla_blocked_ip 123.123.123.1 --permanent
1040 *
1041 * @param array $args Command arguments.
1042 * @param array $assoc_args Associative arguments.
1043 * @uses handleLlaTableEntry()
1044 */
1045 public function add_lla_blocked_ip( $args, $assoc_args ) {
1046 if ( ! $this->check_pro_command_preconditions() ) return;
1047 $status = (isset($assoc_args['permanent']) ? 'blocked' : 'locked');
1048 $this->handleLlaTableEntry($args, $status, 'source_ip', 'add');
1049 }
1050
1051 /**
1052 * Remove an IP from the LLA's blocklist.
1053 *
1054 * Usage: wp rsssl remove_lla_blocked_ip 123.123.123.1
1055 * Usage: wp rsssl remove_lla_blocked_ip 123.123.123.1 --permanent
1056 *
1057 * @param array $args Command arguments.
1058 * @param array $assoc_args Associative arguments.
1059 * @uses handleLlaTableEntry()
1060 */
1061 public function remove_lla_blocked_ip( $args, $assoc_args ) {
1062 if ( ! $this->check_pro_command_preconditions() ) return;
1063 $status = (isset($assoc_args['permanent']) ? 'blocked' : 'locked');
1064 $this->handleLlaTableEntry($args, $status, 'source_ip', 'remove');
1065 }
1066
1067 /**
1068 * Add a username to the LLA's trusted list.
1069 *
1070 * Usage: wp rsssl add_lla_trusted_username username
1071 *
1072 * @param array $args Command arguments.
1073 * @uses handleLlaTableEntry()
1074 */
1075 public function add_lla_trusted_username( $args ) {
1076 if ( ! $this->check_pro_command_preconditions() ) return;
1077 $this->handleLlaTableEntry($args, 'allowed', 'username', 'add');
1078 }
1079
1080 /**
1081 * Remove a username to the LLA's trusted list.
1082 *
1083 * Usage: wp rsssl remove_lla_trusted_username username
1084 *
1085 * @param array $args Command arguments.
1086 * @uses handleLlaTableEntry()
1087 */
1088 public function remove_lla_trusted_username( $args ) {
1089 if ( ! $this->check_pro_command_preconditions() ) return;
1090 $this->handleLlaTableEntry($args, 'allowed', 'username', 'remove');
1091 }
1092
1093 /**
1094 * Add a username to the LLA's blocked list.
1095 *
1096 * Usage: wp rsssl add_lla_blocked_username username
1097 * Usage: wp rsssl add_lla_blocked_username username --permanent
1098 *
1099 * @param array $args Command arguments.
1100 * @param array $assoc_args Associative arguments.
1101 * @uses handleLlaTableEntry()
1102 */
1103 public function add_lla_blocked_username( array $args, array $assoc_args ) {
1104 if ( ! $this->check_pro_command_preconditions() ) return;
1105 $status = (isset($assoc_args['permanent']) ? 'blocked' : 'locked');
1106 $this->handleLlaTableEntry($args, $status, 'username', 'add');
1107 }
1108
1109 /**
1110 * Remove a username to the LLA's blocked list.
1111 *
1112 * Usage: wp rsssl remove_lla_blocked_username username
1113 * Usage: wp rsssl remove_lla_blocked_username username --permanent
1114 *
1115 * @param array $args Command arguments.
1116 * @param array $assoc_args Associative arguments.
1117 * @uses handleLlaTableEntry()
1118 */
1119 public function remove_lla_blocked_username( $args, $assoc_args ) {
1120 if ( ! $this->check_pro_command_preconditions() ) return;
1121 $status = (isset($assoc_args['permanent']) ? 'blocked' : 'locked');
1122 $this->handleLlaTableEntry($args, $status, 'username', 'remove');
1123 }
1124
1125 /**
1126 * Handle an action for the firewall table for a specific IP address.
1127 *
1128 * @param array $args Command arguments.
1129 * @param array $assoc_args Associative arguments.
1130 * @param string $status Should be either 'trusted' or 'blocked'.
1131 * @param string $action Should be either 'add' or 'remove'.
1132 *
1133 * @uses remove_white_list_ip() & add_white_list_ip() from Rsssl_Geo_Block -
1134 * Those also handle a block request for an IP address.
1135 */
1136 protected function handleFirewallTableEntry(array $args, array $assoc_args, string $status, string $action)
1137 {
1138 if (rsssl_get_option('enable_firewall', false) !== true) {
1139 WP_CLI::error('The firewall is not enabled.', true);
1140 }
1141
1142 if (!in_array($status, ['trusted', 'blocked']) || !in_array($action, ['add', 'remove'])) {
1143 WP_CLI::error('Could not handle action for the firewall table.', true);
1144 }
1145
1146 if (empty($args[0])) {
1147 WP_CLI::error('Please provide an IP address.', true);
1148 }
1149
1150 $ip = $this->getFilteredIpAddress($args[0]);
1151
1152 // Prepare data for adding to the whitelist.
1153 $data = [
1154 'ip_address' => $ip,
1155 'note' => $assoc_args['note'] ?? '',
1156 'status' => $status,
1157 'permanent' => isset($assoc_args['permanent']),
1158 ];
1159
1160 // Use the Rsssl_Geo_Block class to add the trusted IP.
1161 if (!class_exists('\RSSSL\Pro\Security\WordPress\Rsssl_Geo_Block')) {
1162 require_once rsssl_path . 'pro/security/wordpress/rsssl-geo-block.php';
1163 }
1164
1165 try {
1166 $geo_block = new \RSSSL\Pro\Security\WordPress\Rsssl_Geo_Block();
1167
1168 // fallback
1169 $response = ['success' => false, 'message' => 'Something went wrong!'];
1170
1171 if ($action === 'remove') {
1172 $response = $geo_block->remove_white_list_ip( $data );
1173 }
1174
1175 if ($action === 'add') {
1176 $response = $geo_block->add_white_list_ip( $data );
1177 }
1178 } catch ( \Exception $e ) {
1179 WP_CLI::error( 'Failed to handle IP entry: ' . $e->getMessage(), true );
1180 }
1181
1182 // Handle response.
1183 if ( $response['success'] ) {
1184 WP_CLI::success( $response['message'] );
1185 return;
1186 }
1187
1188 WP_CLI::error( $response['message'], true );
1189 }
1190
1191 /**
1192 * Handle an action for the LLA table for a specific IP address.
1193 *
1194 * @param array $args Command arguments.
1195 * @param string $status Should be either 'allowed' or 'blocked'.
1196 * @param string $type Should be either 'source_ip' or 'username'.
1197 * @param string $action Should be either 'add' or 'remove'.
1198 * @return void
1199 */
1200 protected function handleLlaTableEntry(array $args, string $status, string $type, string $action): void
1201 {
1202 if (rsssl_get_option('enable_limited_login_attempts', false) !== true) {
1203 WP_CLI::error('The LLA feature is not enabled.', true);
1204 }
1205
1206 if (empty($args[0])) {
1207 WP_CLI::error('Please provide the command the necessary arguments', true);
1208 }
1209
1210 if (!in_array($status, ['allowed', 'blocked', 'locked']) || !in_array($type, ['source_ip', 'username'])) {
1211 WP_CLI::error('Something went wrong! Could not handle command.', true);
1212 }
1213
1214 $value = '';
1215 if ($type === 'source_ip') {
1216 $value = $this->getFilteredIpAddress($args[0]);
1217 }
1218
1219 if ($type === 'username') {
1220 $value = sanitize_text_field($args[0]);
1221 }
1222
1223 // Use the Rsssl_Limit_Login_Admin class to add the trusted IP.
1224 if (!class_exists('\RSSSL\Pro\Security\WordPress\Rsssl_Limit_Login_Admin')) {
1225 require_once rsssl_path . 'pro/security/wordpress/class-rsssl-limit-login-admin.php';
1226 }
1227
1228 try {
1229 $lla = new \RSSSL\Pro\Security\WordPress\Rsssl_Limit_Login_Admin();
1230
1231 // fallback
1232 $response = ['success' => false, 'message' => 'Something went wrong!'];
1233
1234 if ($action === 'add') {
1235 $response = $lla->handle_entity([
1236 'value' => $value,
1237 'status' => sanitize_text_field($status),
1238 ], $type);
1239 }
1240
1241 if ($action === 'remove') {
1242 $entry = $lla->get_entry($type, $value, $status);
1243 $response = $lla->delete_entries([
1244 'id' => $entry['id'],
1245 ]);
1246 }
1247 } catch ( Exception $e ) {
1248 WP_CLI::error( 'Failed to handle LLA entry: ' . $e->getMessage(), true );
1249 }
1250
1251 // Handle response.
1252 if ( $response['success'] ) {
1253 WP_CLI::success( $response['message'] );
1254 return;
1255 }
1256
1257 WP_CLI::error( $response['message'], true );
1258 }
1259
1260 /**
1261 * Return a filtered IP address. Method will exit() if the IP address is
1262 * invalid with the WP_CLI error message: Invalid IP address provided.
1263 */
1264 protected function getFilteredIpAddress(string $originalIp): string
1265 {
1266 // Check if the input is potentially a CIDR
1267 if (strpos($originalIp, '/') !== false) {
1268 list($address, $mask_str) = explode('/', $originalIp, 2);
1269
1270 // Validate the IP address part
1271 if (!filter_var($address, FILTER_VALIDATE_IP)) {
1272 WP_CLI::error('Invalid IP address part in CIDR notation: ' . $address, true);
1273 }
1274
1275 // Validate the mask part
1276 if (!is_numeric($mask_str)) {
1277 WP_CLI::error('CIDR mask is not numeric: ' . $mask_str, true);
1278 }
1279 $mask = (int)$mask_str;
1280
1281 // Determine IP version for mask validation
1282 $is_ipv4 = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
1283 $is_ipv6 = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
1284
1285 if ($is_ipv4) {
1286 if ($mask < 0 || $mask > 32) {
1287 WP_CLI::error('Invalid IPv4 CIDR mask (must be 0-32): ' . $mask, true);
1288 }
1289 } elseif ($is_ipv6) {
1290 if ($mask < 0 || $mask > 128) {
1291 WP_CLI::error('Invalid IPv6 CIDR mask (must be 0-128): ' . $mask, true);
1292 }
1293 } else {
1294 // This case should ideally not be reached if filter_var($address, FILTER_VALIDATE_IP) passed
1295 WP_CLI::error('Unknown IP address type for CIDR validation: ' . $address, true);
1296 }
1297
1298 // If all checks pass for CIDR, return the original CIDR string
1299 return $originalIp;
1300
1301 } else {
1302 // Validate as a plain IP address
1303 $ip = filter_var($originalIp, FILTER_VALIDATE_IP);
1304 if (empty($ip)) {
1305 WP_CLI::error('Invalid IP address provided: ' . $originalIp, true);
1306 }
1307 return $ip;
1308 }
1309 }
1310
1311 /**
1312 * Performs pre-flight checks before SSL activation.
1313 * Checks for HTTPS reachability and potentially other issues like .htaccess writability.
1314 *
1315 * @return array ['success' => bool, 'message' => string, 'warnings' => array]
1316 */
1317 private function perform_pre_flight_checks(): array {
1318 $warnings = [];
1319 $message = '';
1320
1321 // --- Check 1: HTTPS Reachability ---
1322 $home_url = home_url();
1323 $https_url = set_url_scheme( $home_url, 'https' );
1324
1325 // Use wp_remote_get to see if the HTTPS version is reachable
1326 // 'sslverify' => false is important for local/staging with self-signed certs
1327 // Timeout set low to avoid long waits on failure
1328 $response = wp_remote_get( $https_url, [
1329 'timeout' => 10, // seconds
1330 'sslverify' => false,
1331 'redirection' => 5, // Follow redirects
1332 ] );
1333
1334 if ( is_wp_error( $response ) ) {
1335 $error_code = $response->get_error_code();
1336 $error_message = $response->get_error_message();
1337 $friendly_message = sprintf(
1338 __( 'Failed to reach %s. The site does not appear to be accessible over HTTPS. Please ensure your server is configured for SSL.', 'really-simple-ssl' ),
1339 $https_url
1340 );
1341
1342 // Check if WP_DEBUG is enabled
1343 $wp_debug_enabled = ( defined( 'WP_DEBUG' ) && WP_DEBUG );
1344
1345 if ( $wp_debug_enabled ) {
1346 // Log the detailed error when WP_DEBUG is on
1347 // Using WP_CLI::debug requires the --debug flag for wp-cli command itself
1348 WP_CLI::debug( sprintf( "HTTPS Check Error Details: Code=%s, Message=%s", $error_code, $error_message ), 'rsssl-cli-debug' );
1349 // Alternatively, or in addition, use standard PHP error logging:
1350 // error_log( sprintf("Really Simple SSL WP-CLI HTTPS Check Error: Code=%s, Message=%s", $error_code, $error_message) );
1351
1352 // Optionally, still show a slightly more informative message than the friendly one
1353 $message_to_show = sprintf(
1354 __( 'Failed to reach %s. The site does not appear to be accessible over HTTPS (Error: %s). Check debug logs for details.', 'really-simple-ssl' ),
1355 $https_url,
1356 $error_code // Show the code, but maybe not the full verbose message
1357 );
1358 } else {
1359 // Show only the user-friendly message if WP_DEBUG is off
1360 $message_to_show = $friendly_message;
1361 }
1362
1363 return [
1364 'success' => false,
1365 'message' => $message_to_show,
1366 'warnings' => $warnings
1367 ];
1368
1369 } else {
1370 // Connected, check the response code
1371 $response_code = wp_remote_retrieve_response_code( $response );
1372 if ( $response_code < 200 || $response_code >= 400 ) {
1373 // Reached server, but got an error response (e.g., 404 Not Found, 500 Internal Server Error)
1374 return [
1375 'success' => false,
1376 'message' => sprintf( __( 'Reached %s, but received an error response code: %d. HTTPS is not properly configured.', 'really-simple-ssl' ), $https_url, $response_code ),
1377 'warnings' => $warnings
1378 ];
1379 }
1380 // If response code is 2xx or 3xx, we consider HTTPS reachable.
1381 // A more robust check could analyze the body for expected content, but this is usually sufficient.
1382 }
1383
1384 // --- Check 2: .htaccess Writability (if needed) ---
1385 // Keep the previous check for .htaccess if the redirect method is set to htaccess
1386 // $htaccess_writable = true; // Replace with actual check logic (e.g., check if WP_Filesystem allows writing)
1387 if ( rsssl_get_option('redirect') === 'htaccess' ) {
1388 // Get the path to the .htaccess file
1389 $htaccess_file = RSSSL()->admin->htaccess_file(); // Assuming a method to get the correct path
1390 if ( ! is_writable( $htaccess_file ) ) {
1391 $warnings[] = sprintf( __( '.htaccess file (%s) is not writable. Redirects cannot be configured automatically.', 'really-simple-ssl' ), $htaccess_file );
1392 // This remains a warning, as activation might still work partially (WP URLs change)
1393 }
1394 }
1395
1396 // Add more checks as needed (e.g., specific certificate details if possible/required)...
1397
1398 $message = __( 'Pre-flight checks passed.', 'really-simple-ssl' );
1399 return ['success' => true, 'message' => $message, 'warnings' => $warnings];
1400 }
1401
1402 /**
1403 * Get command details for WP-CLI commands.
1404 *
1405 * @return array Command details.
1406 */
1407 protected function get_command_list() {
1408 return [
1409 'activate_ssl' => [
1410 'description' => __( 'Activate SSL on the site.', 'really-simple-ssl' ),
1411 'synopsis' => [],
1412 'pro' => false,
1413 ],
1414 'deactivate_ssl' => [
1415 'description' => __( 'Deactivate SSL on the site.', 'really-simple-ssl' ),
1416 'synopsis' => [],
1417 'pro' => false,
1418 ],
1419 'update_option' => [
1420 'description' => __( 'Update a Really Simple Security option. Usage: wp rsssl update_option --name=option_name --value=option_value. Use 0 and 1 for booleans.', 'really-simple-ssl' ),
1421 'synopsis' => [
1422 [
1423 'type' => 'assoc',
1424 'name' => 'name',
1425 'optional' => false,
1426 'description' => __( 'Name of the option to update.', 'really-simple-ssl' ),
1427 ],
1428 [
1429 'type' => 'assoc',
1430 'name' => 'value',
1431 'optional' => false,
1432 'description' => __( 'Value to set for the option.', 'really-simple-ssl' ),
1433 ],
1434 ],
1435 'pro' => false,
1436 ],
1437 'activate_recommended_features' => [
1438 'description' => __( 'Activate all recommended features.', 'really-simple-ssl' ),
1439 'synopsis' => [],
1440 'pro' => false,
1441 ],
1442 'deactivate_recommended_features' => [
1443 'description' => __( 'Deactivate all recommended features.', 'really-simple-ssl' ),
1444 'synopsis' => [],
1445 'pro' => false,
1446 ],
1447 'activate_security_headers' => [
1448 'description' => __( 'Activate essential security headers.', 'really-simple-ssl' ),
1449 'synopsis' => [],
1450 'pro' => true,
1451 ],
1452 'deactivate_security_headers' => [
1453 'description' => __( 'Deactivate essential security headers.', 'really-simple-ssl' ),
1454 'synopsis' => [],
1455 'pro' => true,
1456 ],
1457 'activate_firewall' => [
1458 'description' => __( 'Activate the firewall.', 'really-simple-ssl' ),
1459 'synopsis' => [],
1460 'pro' => true,
1461 ],
1462 'deactivate_firewall' => [
1463 'description' => __( 'Deactivate the firewall.', 'really-simple-ssl' ),
1464 'synopsis' => [],
1465 'pro' => true,
1466 ],
1467 'activate_2fa' => [
1468 'description' => __( 'Activate Two-Factor Authentication.', 'really-simple-ssl' ),
1469 'synopsis' => [],
1470 'pro' => false,
1471 ],
1472 'deactivate_2fa' => [
1473 'description' => __( 'Deactivate Two-Factor Authentication.', 'really-simple-ssl' ),
1474 'synopsis' => [],
1475 'pro' => false,
1476 ],
1477 'activate_password_security' => [
1478 'description' => __( 'Activate password security features.', 'really-simple-ssl' ),
1479 'synopsis' => [],
1480 'pro' => true,
1481 ],
1482 'deactivate_password_security' => [
1483 'description' => __( 'Deactivate password security features.', 'really-simple-ssl' ),
1484 'synopsis' => [],
1485 'pro' => true,
1486 ],
1487 'activate_lla' => [
1488 'description' => __( 'Activate limit login attempts.', 'really-simple-ssl' ),
1489 'synopsis' => [],
1490 'pro' => true,
1491 ],
1492 'deactivate_lla' => [
1493 'description' => __( 'Deactivate limit login attempts.', 'really-simple-ssl' ),
1494 'synopsis' => [],
1495 'pro' => true,
1496 ],
1497 'activate_vulnerability_scanning' => [
1498 'description' => __( 'Activate vulnerability scanning.', 'really-simple-ssl' ),
1499 'synopsis' => [],
1500 'pro' => false,
1501 ],
1502 'deactivate_vulnerability_scanning' => [
1503 'description' => __( 'Deactivate vulnerability scanning.', 'really-simple-ssl' ),
1504 'synopsis' => [],
1505 'pro' => false,
1506 ],
1507 'activate_license' => [
1508 'description' => __( 'Activate a license key. Usage: wp rsssl activate_license YOUR_LICENSE_KEY.', 'really-simple-ssl' ),
1509 'synopsis' => [
1510 [
1511 'type' => 'positional',
1512 'name' => 'license_key',
1513 'optional' => false,
1514 'description' => __( 'The license key to activate.', 'really-simple-ssl' ),
1515 ],
1516 ],
1517 'pro' => true,
1518 ],
1519 'deactivate_license' => [
1520 'description' => __( 'Deactivate the license.', 'really-simple-ssl' ),
1521 'synopsis' => [],
1522 'pro' => true,
1523 ],
1524 'add_lock_file' => [
1525 'description' => __( 'Add a lock file for safe mode.', 'really-simple-ssl' ),
1526 'synopsis' => [],
1527 'pro' => false,
1528 ],
1529 'remove_lock_file' => [
1530 'description' => __( 'Remove the lock file for safe mode.', 'really-simple-ssl' ),
1531 'synopsis' => [],
1532 'pro' => false,
1533 ],
1534 'reset_2fa' => [
1535 'description' => __( 'Reset the 2FA status and methods for a user.', 'really-simple-ssl' ),
1536 'synopsis' => [
1537 [
1538 'type' => 'positional',
1539 'name' => 'user_id',
1540 'optional' => false,
1541 'description' => __( 'The user ID to reset 2FA for.', 'really-simple-ssl' ),
1542 ],
1543 ],
1544 'pro' => false,
1545 ],
1546 'twofa_preview' => [
1547 'description' => __( 'Preview users in scope for 2FA reminders (dry-run).', 'really-simple-ssl' ),
1548 'synopsis' => [
1549 [ 'type' => 'assoc', 'name' => 'role', 'optional' => true, 'description' => __( 'Filter by user role (default: all).', 'really-simple-ssl' ) ],
1550 [ 'type' => 'assoc', 'name' => 'site', 'optional' => true, 'description' => __( 'Limit to a single blog_id (multisite).', 'really-simple-ssl' ) ],
1551 [ 'type' => 'flag', 'name' => 'include-subsites', 'optional' => true, 'description' => __( 'Traverse all subsites in the network.', 'really-simple-ssl' ) ],
1552 [ 'type' => 'assoc', 'name' => 'format','optional' => true, 'description' => __( 'Output format: table|json|csv (default: table).', 'really-simple-ssl' ) ],
1553 [ 'type' => 'flag', 'name' => 'reset-meta', 'optional' => true, 'description' => __( 'Reset rsssl_two_fa_reminder_sent meta for a clean run.', 'really-simple-ssl' ) ],
1554 ],
1555 'pro' => true,
1556 ],
1557 'twofa_send' => [
1558 'description' => __( 'Send 2FA reminders for the current selection.', 'really-simple-ssl' ),
1559 'synopsis' => [
1560 [ 'type' => 'assoc', 'name' => 'role', 'optional' => true, 'description' => __( 'Filter by user role (default: all).', 'really-simple-ssl' ) ],
1561 [ 'type' => 'assoc', 'name' => 'site', 'optional' => true, 'description' => __( 'Limit to a single blog_id (multisite).', 'really-simple-ssl' ) ],
1562 [ 'type' => 'flag', 'name' => 'include-subsites', 'optional' => true, 'description' => __( 'Traverse all subsites in the network.', 'really-simple-ssl' ) ],
1563 [ 'type' => 'flag', 'name' => 'reset-meta', 'optional' => true, 'description' => __( 'Reset rsssl_two_fa_reminder_sent meta before sending.', 'really-simple-ssl' ) ],
1564 ],
1565 'pro' => true,
1566 ],
1567 'update_advanced_headers' => [
1568 'description' => __( 'Update the advanced-headers.php with the latest rules.', 'really-simple-ssl' ),
1569 'synopsis' => [],
1570 'pro' => false,
1571 ],
1572 'add_firewall_ip_block' => [
1573 'description' => __( 'Add IP block.', 'really-simple-ssl' ),
1574 'synopsis' => [
1575 [
1576 'type' => 'positional',
1577 'name' => 'ip_address',
1578 'optional' => false,
1579 'description' => __( 'The IP to block.', 'really-simple-ssl' ),
1580 ],
1581 [
1582 'type' => 'flag',
1583 'name' => 'permanent',
1584 'optional' => true,
1585 'description' => __( 'Flag to add a permanent block.', 'really-simple-ssl' ),
1586 ],
1587 [
1588 'type' => 'assoc',
1589 'name' => 'note',
1590 'optional' => true,
1591 'description' => __( 'Optional note for the block.', 'really-simple-ssl' ),
1592 ],
1593 ],
1594 'pro' => true,
1595 ],
1596 'remove_firewall_ip_block' => [
1597 'description' => __( 'Remove IP block.', 'really-simple-ssl' ),
1598 'synopsis' => [
1599 [
1600 'type' => 'positional',
1601 'name' => 'ip_address',
1602 'optional' => false,
1603 'description' => __( 'The IP to remove the block for.', 'really-simple-ssl' ),
1604 ],
1605 ],
1606 'pro' => true,
1607 ],
1608 'show_blocked_ips' => [
1609 'description' => __( 'Show blocked IP\'s.', 'really-simple-ssl' ),
1610 'synopsis' => [],
1611 'pro' => true,
1612 ],
1613 'add_firewall_trusted_ip' => [
1614 'description' => __( 'Add a trusted IP to the firewall.', 'really-simple-ssl' ),
1615 'synopsis' => [],
1616 'pro' => true,
1617 ],
1618 'remove_firewall_trusted_ip' => [
1619 'description' => __( 'Remove a trusted IP from the firewall.', 'really-simple-ssl' ),
1620 'synopsis' => [],
1621 'pro' => true,
1622 ],
1623 'add_lla_trusted_ip' => [
1624 'description' => __( 'Add a trusted IP to the limit login attempts table.', 'really-simple-ssl' ),
1625 'synopsis' => [],
1626 'pro' => true,
1627 ],
1628 'remove_lla_trusted_ip' => [
1629 'description' => __( 'Remove a trusted IP from the limit login attempts table.', 'really-simple-ssl' ),
1630 'synopsis' => [],
1631 'pro' => true,
1632 ],
1633 'add_lla_blocked_ip' => [
1634 'description' => __( 'Add a blocked IP to the limit login attempts table.', 'really-simple-ssl' ),
1635 'synopsis' => [
1636 [
1637 'type' => 'positional',
1638 'name' => 'ip_address',
1639 'optional' => false,
1640 'description' => __( 'The IP to block.', 'really-simple-ssl' ),
1641 ],
1642 [
1643 'type' => 'flag',
1644 'name' => 'permanent',
1645 'optional' => true,
1646 'description' => __( 'Flag to add a permanent block.', 'really-simple-ssl' ),
1647 ],
1648 ],
1649 'pro' => true,
1650 ],
1651 'remove_lla_blocked_ip' => [
1652 'description' => __( 'Remove a blocked IP from the limit login attempts table.', 'really-simple-ssl' ),
1653 'synopsis' => [
1654 [
1655 'type' => 'positional',
1656 'name' => 'ip_address',
1657 'optional' => false,
1658 'description' => __( 'The IP to block.', 'really-simple-ssl' ),
1659 ],
1660 [
1661 'type' => 'flag',
1662 'name' => 'permanent',
1663 'optional' => true,
1664 'description' => __( 'Flag to add a permanent block.', 'really-simple-ssl' ),
1665 ],
1666 ],
1667 'pro' => true,
1668 ],
1669 'add_lla_trusted_username' => [
1670 'description' => __( 'Add a trusted username to the limit login attempts table.', 'really-simple-ssl' ),
1671 'synopsis' => [],
1672 'pro' => true,
1673 ],
1674 'remove_lla_trusted_username' => [
1675 'description' => __( 'Remove a trusted username from the limit login attempts table.', 'really-simple-ssl' ),
1676 'synopsis' => [],
1677 'pro' => true,
1678 ],
1679 'add_lla_blocked_username' => [
1680 'description' => __( 'Add a blocked username to the limit login attempts table.', 'really-simple-ssl' ),
1681 'synopsis' => [
1682 [
1683 'type' => 'positional',
1684 'name' => 'ip_address',
1685 'optional' => false,
1686 'description' => __( 'The username to block.', 'really-simple-ssl' ),
1687 ],
1688 [
1689 'type' => 'flag',
1690 'name' => 'permanent',
1691 'optional' => true,
1692 'description' => __( 'Flag to add a permanent block.', 'really-simple-ssl' ),
1693 ],
1694 ],
1695 'pro' => true,
1696 ],
1697 'remove_lla_blocked_username' => [
1698 'description' => __( 'Remove a blocked username from the limit login attempts table.', 'really-simple-ssl' ),
1699 'synopsis' => [
1700 [
1701 'type' => 'positional',
1702 'name' => 'username',
1703 'optional' => false,
1704 'description' => __( 'The username to remove the block for.', 'really-simple-ssl' ),
1705 ],
1706 [
1707 'type' => 'flag',
1708 'name' => 'permanent',
1709 'optional' => true,
1710 'description' => __( 'Flag to remove a permanent block.', 'really-simple-ssl' ),
1711 ],
1712 ],
1713 'pro' => true,
1714 ],
1715 ];
1716 }
1717
1718 /**
1719 * This method registers our WP-CLI commands and uses {@see get_command_list()}
1720 * to retrieve the list. Do not execute this method before the init hook.
1721 */
1722 public function register_wp_cli_commands() {
1723 $command_details = $this->get_command_list();
1724 foreach ( $command_details as $command => $details ) {
1725 if ( isset( $details['inactive'] ) && $details['inactive'] === true ) {
1726 continue;
1727 }
1728 WP_CLI::add_command(
1729 "rsssl $command",
1730 [ $this, $command ],
1731 [
1732 'shortdesc' => $details['description'],
1733 'synopsis' => $details['synopsis'],
1734 ]
1735 );
1736 }
1737 }
1738 }
1739
1740 // Add devtools command if present
1741 if ( file_exists( rsssl_path . 'pro/assets/tools/cli/class-rsssl-stub-generator.php' ) ) {
1742 require_once rsssl_path . 'pro/assets/tools/cli/class-rsssl-stub-generator.php';
1743 }