PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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
defender-security / src / controller / class-security-tweaks.php

class-security-tweaks.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/controller/class-security-tweaks.php

1,116 lines 29.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles security tweaks operations.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use Countable;
11 use WP_Defender\Event;
12 use WP_Defender\Admin;
13 use Calotes\Component\Request;
14 use Calotes\Component\Response;
15 use Calotes\Helper\Array_Cache;
16 use WP_Defender\Model\Notification\Tweak_Reminder;
17 use WP_Defender\Component\Config\Config_Hub_Helper;
18 use WP_Defender\Component\Security_Tweaks\Hide_Error;
19 use WP_Defender\Component\Security_Tweaks\WP_Version;
20 use WP_Defender\Component\Security_Tweaks\PHP_Version;
21 use WP_Defender\Component\Security_Tweaks\Prevent_PHP;
22 use WP_Defender\Component\Security_Tweaks\Change_Admin;
23 use WP_Defender\Component\Security_Tweaks\Security_Key;
24 use WP_Defender\Component\Security_Tweaks\Login_Duration;
25 use WP_Defender\Component\Security_Tweaks\Servers\Server;
26 use WP_Defender\Component\Security_Tweaks\Disable_XML_RPC;
27 use WP_Defender\Component\Security_Tweaks\Disable_Trackback;
28 use WP_Defender\Component\Security_Tweaks\Prevent_Enum_Users;
29 use WP_Defender\Component\Security_Tweaks\Disable_File_Editor;
30 use WP_Defender\Component\Security_Tweaks\Protect_Information;
31 use WP_Defender\Model\Setting\Security_Tweaks as Model_Security_Tweaks;
32
33 /**
34 * Contains methods for processing, reverting, ignoring security recommendations, and updating security reminders.
35 */
36 class Security_Tweaks extends Event {
37
38 /**
39 * The slug identifier for this controller.
40 *
41 * @var string
42 */
43 public $slug = 'wdf-hardener';
44
45 /**
46 * The model for handling the data.
47 *
48 * @var Model_Security_Tweaks
49 */
50 protected $model;
51
52 /**
53 * Components instance array.
54 *
55 * @var array
56 */
57 private $component_instances;
58
59 /**
60 * Instance of Security_Key.
61 *
62 * @var Security_Key
63 */
64 private $security_key;
65
66 /**
67 * Instance of Prevent_Enum_Users.
68 *
69 * @var Prevent_Enum_Users
70 */
71 private $prevent_enum_users;
72
73 public const STATUS_ISSUES = 'issues', STATUS_RESOLVE = 'fixed', STATUS_IGNORE = 'ignore', STATUS_RESTORE = 'restore';
74
75 /**
76 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
77 */
78 public function __construct() {
79 $this->register_page(
80 esc_html__( 'Recommendations', 'defender-security' ),
81 $this->slug,
82 array( $this, 'main_view' ),
83 $this->parent_slug
84 );
85 $this->model = wd_di()->get( Model_Security_Tweaks::class );
86 $this->register_routes();
87
88 // Init all the tweaks, should happen one time.
89 $this->component_instances = $this->init_tweaks();
90
91 $this->security_key = $this->component_instances['security-key'];
92 $this->prevent_enum_users = $this->component_instances['prevent-enum-users'];
93
94 // Now shield up.
95 $this->boot();
96 // Add addition hooks.
97 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
98 add_action( 'wp_loaded', array( $this, 'should_output_error' ) );
99 }
100
101 /**
102 * Dummy function for testing a check.
103 */
104 public function should_output_error() {
105 if ( empty( defender_get_data_from_request( 'defender_test_error_reporting', 'g' ) ) ) {
106 return;
107 }
108 // It should be only trigger by admin.
109 if ( ! $this->check_permission() ) {
110 return;
111 }
112
113 $var = '$' . uniqid( '', true );
114 // This should output a warning. Ignored phpcs as it's a test.
115 echo ${$var}; // phpcs:ignore
116 exit();
117 }
118
119 /**
120 * Process.
121 *
122 * @param Request $request Request object.
123 *
124 * @return Response
125 * @defender_route
126 */
127 public function process( Request $request ) {
128 $data = $request->get_data(
129 array(
130 'slug' => array(
131 'type' => 'string',
132 'sanitize' => 'sanitize_text_field',
133 ),
134 'current_server' => array(
135 'type' => 'string',
136 'sanitize' => 'sanitize_text_field',
137 ),
138 )
139 );
140
141 $slug = $data['slug'] ?? false;
142 $tweak = $this->get_tweak( $slug );
143
144 if ( ! is_object( $tweak ) ) {
145 return new Response(
146 false,
147 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
148 );
149 }
150
151 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
152 $current_server = $data['current_server'] ?? false;
153 if ( ! $current_server ) {
154 return new Response(
155 false,
156 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
157 );
158 }
159
160 $ret = $tweak->process( $current_server );
161 } else {
162 $ret = $tweak->process();
163 }
164
165 if ( true === $ret ) {
166 Config_Hub_Helper::set_clear_active_flag();
167 $this->model->mark( self::STATUS_RESOLVE, $slug );
168 // Track.
169 $this->track_tweak( $tweak->get_label(), 'Actioned' );
170 // Response.
171 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) );
172 }
173 if ( is_wp_error( $ret ) ) {
174 $this->ajax_response( $ret->get_error_message(), false );
175 }
176
177 return new Response(
178 false,
179 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
180 );
181 }
182
183 /**
184 * Revert.
185 *
186 * @param Request $request Request object.
187 *
188 * @return Response
189 * @defender_route
190 */
191 public function revert( Request $request ) {
192 $data = $request->get_data(
193 array(
194 'slug' => array(
195 'type' => 'string',
196 'sanitize' => 'sanitize_text_field',
197 ),
198 'current_server' => array(
199 'type' => 'string',
200 'sanitize' => 'sanitize_text_field',
201 ),
202 )
203 );
204 $slug = $data['slug'] ?? false;
205 $tweak = $this->get_tweak( $slug );
206 $invalid = array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) );
207 if ( ! is_object( $tweak ) ) {
208 return new Response(
209 false,
210 $invalid
211 );
212 }
213 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
214 $current_server = $data['current_server'] ?? false;
215 if ( ! $current_server ) {
216 return new Response(
217 false,
218 $invalid
219 );
220 }
221 $ret = $tweak->revert( $current_server );
222 } else {
223 $ret = $tweak->revert();
224 }
225
226 if ( is_wp_error( $ret ) ) {
227 $this->ajax_response( $ret->get_error_message(), false );
228 }
229 if ( true === $ret ) {
230 Config_Hub_Helper::set_clear_active_flag();
231 $this->model->mark( self::STATUS_ISSUES, $slug );
232 // Track.
233 $this->track_tweak( $tweak->get_label(), 'Reverted' );
234 // Response.
235 $this->ajax_response( esc_html__( 'Security recommendation successfully reverted.', 'defender-security' ) );
236 }
237
238 return new Response(
239 false,
240 $invalid
241 );
242 }
243
244 /**
245 * Ignore.
246 *
247 * @param Request $request Request object.
248 *
249 * @return Response|void
250 * @defender_route
251 */
252 public function ignore( Request $request ) {
253 $data = $request->get_data(
254 array(
255 'slug' => array(
256 'type' => 'string',
257 'sanitize' => 'sanitize_text_field',
258 ),
259 )
260 );
261 $slug = $data['slug'] ?? false;
262 $tweak = $this->get_tweak( $slug );
263 if ( ! is_object( $tweak ) ) {
264 return new Response(
265 false,
266 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
267 );
268 }
269 $this->model->mark( self::STATUS_IGNORE, $slug );
270 // Track.
271 $this->track_tweak( $tweak->get_label(), 'Ignored' );
272 if ( 'security-key' === $slug ) {
273 $this->security_key->cron_unschedule();
274 }
275
276 $this->ajax_response( esc_html__( 'Security recommendation successfully ignored.', 'defender-security' ) );
277 }
278
279 /**
280 * Restore.
281 *
282 * @param Request $request Request object.
283 *
284 * @return Response|void
285 * @defender_route
286 */
287 public function restore( Request $request ) {
288 $data = $request->get_data(
289 array(
290 'slug' => array(
291 'type' => 'string',
292 'sanitize' => 'sanitize_text_field',
293 ),
294 )
295 );
296 $slug = $data['slug'] ?? false;
297 $tweak = $this->get_tweak( $slug );
298 if ( ! is_object( $tweak ) ) {
299 return new Response(
300 false,
301 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
302 );
303 }
304 $this->model->mark( self::STATUS_RESTORE, $slug );
305 // Track.
306 $this->track_tweak( $tweak->get_label(), 'Restored' );
307
308 if ( $this->security_key->get_is_autogenerate_keys() ) {
309 $this->security_key->cron_schedule();
310 }
311
312 $this->ajax_response( esc_html__( 'Security recommendation successfully restored.', 'defender-security' ) );
313 }
314
315 /**
316 * Recheck.
317 *
318 * @param Request $request Request object.
319 *
320 * @return Response
321 * @defender_route
322 */
323 public function recheck( Request $request ): Response {
324 $data = $request->get_data(
325 array(
326 'slug' => array(
327 'type' => 'string',
328 'sanitize' => 'sanitize_text_field',
329 ),
330 )
331 );
332 $slug = $data['slug'] ?? false;
333 $tweak = $this->get_tweak( $slug );
334
335 if ( ! is_object( $tweak ) ) {
336 return new Response(
337 false,
338 array(
339 'message' => esc_html__(
340 'The status cannot be verified as the request contains an invalid slug.',
341 'defender-security'
342 ),
343 )
344 );
345 }
346
347 $ret = $tweak->check();
348
349 if ( true === $ret ) {
350 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ), true, 1 );
351 }
352
353 if ( is_wp_error( $ret ) ) {
354 return new Response(
355 false,
356 array( 'message' => $ret->get_error_message() )
357 );
358 }
359
360 $msg = sprintf(
361 /* translators: %s: Tweak title. */
362 esc_html__( '%s is not actioned. Please ensure that all the instructions are followed.', 'defender-security' ),
363 $tweak->get_label() ?? ''
364 );
365
366 return new Response(
367 false,
368 array( 'message' => $msg )
369 );
370 }
371
372 /**
373 * Update security reminder.
374 *
375 * @param Request $request Request object.
376 *
377 * @return Response
378 * @defender_route
379 */
380 public function update_security_reminder( Request $request ): Response {
381 $data = $request->get_data();
382 $remind_date = $data['remind_date'] ?? false;
383
384 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
385 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
386 false;
387
388 if ( ! $remind_date ) {
389 return new Response(
390 false,
391 array( 'message' => esc_html__( 'Invalid Reminder frequency', 'defender-security' ) )
392 );
393 }
394
395 $values = array(
396 'reminder_duration' => $remind_date,
397 'reminder_date' => strtotime( '+' . $remind_date, time() ),
398 'is_autogenerate_keys' => $is_autogen_flag,
399 );
400
401 if ( update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values ) ) {
402
403 if ( true === $is_autogen_flag ) {
404 $this->security_key->cron_schedule();
405 } else {
406 $this->security_key->cron_unschedule();
407 }
408
409 return new Response(
410 true,
411 array( 'message' => esc_html__( 'Security recommendation successfully updated.', 'defender-security' ) )
412 );
413 } else {
414 return new Response(
415 false,
416 array( 'message' => esc_html__( 'Error while updating.', 'defender-security' ) )
417 );
418 }
419 }
420
421 /**
422 * AJAX Response handler.
423 *
424 * @param string $message The message to be displayed in the response.
425 * @param bool $is_success Whether the response should have a success status.
426 * @param bool|int $interval The cron interval for the security key autogeneration. Default is false.
427 *
428 * @return Response The AJAX response.
429 */
430 private function ajax_response( $message, $is_success = true, $interval = false ): Response {
431 global $wp_version;
432
433 $settings = new Model_Security_Tweaks();
434 $data = array(
435 'message' => $message,
436 'summary' => array(
437 'issues_count' => count( $settings->issues ),
438 'fixed_count' => count( $settings->fixed ),
439 'ignore_count' => count( $settings->ignore ),
440 'php_version' => PHP_VERSION,
441 'wp_version' => $wp_version,
442 ),
443 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
444 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
445 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
446 'issues_slugs' => $settings->issues,
447 );
448 if ( $interval ) {
449 $data['interval'] = $interval;
450 }
451
452 return new Response( $is_success, $data );
453 }
454
455 /**
456 * Enqueues scripts and styles for this page.
457 * Only enqueues assets if the page is active.
458 */
459 public function enqueue_assets() {
460 if ( ! $this->is_page_active() ) {
461 return;
462 }
463
464 wp_localize_script( 'def-securitytweaks', 'security_tweaks', $this->data_frontend() );
465 wp_enqueue_script( 'def-securitytweaks' );
466 $this->enqueue_main_assets();
467 }
468
469 /**
470 * Provides data for the dashboard widget.
471 *
472 * @return array
473 */
474 public function dashboard_widget(): array {
475 $tweak_arr = $this->model->get_tweak_types();
476 $data = array();
477 foreach ( $this->init_tweaks( self::STATUS_ISSUES ) as $slug => $tweak ) {
478 $data[] = array(
479 'title' => $tweak->get_label(),
480 'slug' => $slug,
481 );
482 }
483
484 return array(
485 'summary' => array(
486 'fixed_count' => $tweak_arr['count_fixed'],
487 'ignore_count' => $tweak_arr['count_ignored'],
488 'issues_count' => $tweak_arr['count_issues'],
489 ),
490 'issues' => $data,
491 );
492 }
493
494 /**
495 * Provides data for the frontend.
496 *
497 * @return array An array of data for the frontend.
498 */
499 public function data_frontend(): array {
500 $this->refresh_tweaks_status();
501 global $wp_version;
502
503 $not_allowed_bulk = array(
504 'php-version',
505 'replace-admin-username',
506 );
507 if ( 'nginx' === Server::get_current_server() ) {
508 $not_allowed_bulk[] = 'protect-information';
509 $not_allowed_bulk[] = 'prevent-php-executed';
510 }
511
512 $tweak_arr = $this->model->get_tweak_types();
513
514 $data = array(
515 'summary' => array(
516 'fixed_count' => $tweak_arr['count_fixed'],
517 'ignore_count' => $tweak_arr['count_ignored'],
518 'issues_count' => $tweak_arr['count_issues'],
519 'php_version' => PHP_VERSION,
520 'wp_version' => $wp_version,
521 ),
522 // Todo: improve the logic so that Tweak initialization happens in one go and return 3 different types.
523 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
524 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
525 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
526 'not_allowed_bulk' => $not_allowed_bulk,
527 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(),
528 'reminder_frequencies' => $this->security_key->reminder_frequencies(),
529 'enabled_user_enums' => $this->prevent_enum_users->get_enabled_user_enums(),
530 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
531 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
532 );
533
534 return array_merge( $data, $this->dump_routes_and_nonces() );
535 }
536
537 /**
538 * Renders the main view for this page.
539 */
540 public function main_view() {
541 $this->render( 'main' );
542 }
543
544 /**
545 * Bulk action for security tweaks.
546 * Handles bulk resolving, ignoring, or reverting security tweaks.
547 *
548 * @param Request $request The request object.
549 *
550 * @return Response
551 * @defender_route
552 */
553 public function bulk_action( Request $request ) {
554 $data = $request->get_data(
555 array(
556 'slugs' => array(
557 'type' => 'array',
558 'sanitize' => 'sanitize_text_field',
559 ),
560 'intention' => array(
561 'type' => 'string',
562 'sanitize' => 'sanitize_text_field',
563 ),
564 )
565 );
566 $slugs = $data['slugs'] ?? array();
567 $intention = $data['intention'] ?? false;
568 // Get processed and unprocessed tweaks.
569 [ $processed, $unprocessed ] = $this->security_tweaks_auto_action( $slugs, $intention );
570
571 $message = sprintf(
572 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
573 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
574 'ignore' === $intention ? 'ignored' : 'resolved',
575 $processed
576 );
577
578 if ( isset( $unprocessed ) && $unprocessed > 0 ) {
579 // If we have this case this mean the intention is resolved.
580 $message = sprintf(
581 /* translators: %d: Count security tweaks */
582 esc_html__(
583 'You have bulk actioned %d security recommendations. You still have a few unresolved security recommendations, which cannot be bulk actioned automatically, so please address them below.',
584 'defender-security'
585 ),
586 $processed
587 );
588
589 Config_Hub_Helper::set_clear_active_flag();
590 }
591
592 return $this->ajax_response( $message );
593 }
594
595 /**
596 * Mass processing.
597 *
598 * @param array $slugs Tweak slugs.
599 * @param string $intention Status.
600 *
601 * @return array
602 */
603 public function security_tweaks_auto_action( $slugs, $intention ): array {
604 $processed = 0;
605 $unprocessed = 0;
606
607 foreach ( $slugs as $slug ) {
608 $tweak = $this->get_tweak( $slug );
609 if ( 'ignore' === $intention ) {
610 $this->model->mark( self::STATUS_IGNORE, $slug );
611 // Track.
612 $this->track_tweak( $tweak->get_label(), 'Ignored' );
613 } elseif ( 'resolve' === $intention ) {
614 $wont_do = array(
615 'replace-admin-username',
616 'prevent-php-executed',
617 'wp-version',
618 'php-version',
619 'protect-information',
620 );
621 if ( in_array( $slug, $wont_do, true ) ) {
622 ++$unprocessed;
623 continue;
624 }
625 if ( $tweak->has_method( 'bulk_process' ) ) {
626 $ret = $tweak->bulk_process();
627 } else {
628 $ret = $tweak->process();
629 }
630 if ( is_wp_error( $ret ) ) {
631 $this->ajax_response(
632 sprintf(
633 /* translators: 1: Security tweak title, 2: Error message */
634 esc_html__(
635 'There is an error while processing recommendation %1$s, error message: %2$s',
636 'defender-security'
637 ),
638 $tweak->get_label(),
639 $ret->get_error_message()
640 ),
641 false
642 );
643 }
644 $this->model->mark( self::STATUS_RESOLVE, $slug );
645 // Track.
646 $this->track_tweak( $tweak->get_label(), 'Actioned' );
647 }
648 ++$processed;
649 }
650
651 return array( $processed, $unprocessed );
652 }
653
654 /**
655 * Refresh the tweak status and save their state.
656 *
657 * @return void
658 */
659 public function refresh_tweaks_status() {
660 $tweaks = $this->init_tweaks();
661 $settings = new Model_Security_Tweaks();
662 $fixed = array();
663 $issues = array();
664
665 foreach ( $tweaks as $slug => $class ) {
666 if ( $settings->is_tweak_ignore( $slug ) ) {
667 continue;
668 }
669
670 $is_resolved = $class->check();
671
672 if ( $is_resolved ) {
673 $fixed[] = $slug;
674 } else {
675 $issues[] = $slug;
676 }
677 }
678
679 $settings->fixed = $fixed;
680 $settings->issues = $issues;
681 $settings->save();
682 }
683
684 /**
685 * This function for shield every active tweaks up, we will use the cached result.
686 * No check function trigger in this init runtime.
687 */
688 private function boot() {
689 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
690 foreach ( $tweaks as $tweak ) {
691 $tweak->shield_up();
692 }
693 }
694
695 /**
696 * Instance all the tweaks, happen one time in init runtime.
697 *
698 * @param string $type Type of tweaks.
699 * @param string $format Object for internal use, array for frontend use.
700 *
701 * @return array
702 */
703 public function init_tweaks( $type = '', $format = 'object' ): array {
704 $classes = array(
705 Disable_XML_RPC::class,
706 WP_Version::class,
707 Hide_Error::class,
708 PHP_Version::class,
709 Change_Admin::class,
710 Security_Key::class,
711 Login_Duration::class,
712 Disable_Trackback::class,
713 Prevent_Enum_Users::class,
714 Disable_File_Editor::class,
715 );
716 if ( ! defender_is_wp_cli() ) {
717 // We don't load this in cli, as clearly no server is running.
718 $classes = array_merge(
719 $classes,
720 array(
721 Protect_Information::class,
722 Prevent_PHP::class,
723 )
724 );
725 }
726
727 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
728
729 if ( ! is_array( $tweaks ) ) {
730 foreach ( $classes as $class ) {
731 $obj = new $class();
732 $tweaks[ $obj->slug ] = $obj;
733 }
734 Array_Cache::set( 'tweaks', $tweaks, 'tweaks' );
735 }
736 $tmp = array();
737 if ( empty( $type ) ) {
738 $tmp = $tweaks;
739 } else {
740 $settings = new Model_Security_Tweaks();
741 $compare = $settings->$type;
742 foreach ( $compare as $slug ) {
743 if ( isset( $tweaks[ $slug ] ) ) {
744 $tmp[ $slug ] = $tweaks[ $slug ];
745 }
746 }
747 }
748
749 if ( 'array' === $format ) {
750 // We need to parse this as array.
751 foreach ( $tmp as $slug => $obj ) {
752 $arr = $obj->to_array();
753 $arr['status'] = $type;
754 $tmp[ $slug ] = $arr;
755 }
756 }
757
758 return $tmp;
759 }
760
761 /**
762 * Get tweak object from cache by slug.
763 *
764 * @param string $slug Tweak slug.
765 *
766 * @return mixed Tweak object if exists, else null.
767 */
768 private function get_tweak( $slug ) {
769 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
770
771 return $tweaks[ $slug ] ?? null;
772 }
773
774 /**
775 * Converts the current object state to an array.
776 *
777 * @return array The array representation of the object.
778 */
779 public function to_array(): array {
780 $this->refresh_tweaks_status();
781 $settings = new Model_Security_Tweaks();
782
783 return array(
784 'count' => array(
785 'issues' => count( $settings->issues ),
786 'resolved' => count( $settings->fixed ),
787 'total' => count( $this->init_tweaks() ),
788 ),
789 );
790 }
791
792 /**
793 * Removes settings for all submodules.
794 */
795 public function remove_settings() {
796 // Revert it first.
797 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
798 // Assign this so internal can use the current server.
799 $_POST['current_server'] = Server::get_current_server();
800 foreach ( $tweaks as $tweak ) {
801 $tweak->revert();
802
803 if ( method_exists( $tweak, 'delete_all_option' ) ) {
804 $tweak->delete_all_option();
805 }
806 }
807
808 ( new Model_Security_Tweaks() )->delete();
809
810 delete_site_transient( Server::CACHE_CURRENT_SERVER );
811 delete_site_transient( \WP_Defender\Component\Security_Tweaks\Servers\Apache::CACHE_APACHE_VERSION );
812 $this->security_key->cron_unschedule();
813 }
814
815 /**
816 * Delete all the data & the cache.
817 */
818 public function remove_data() {
819 // Remove cached data.
820 Array_Cache::remove( 'tweaks', 'tweaks' );
821 delete_site_option( Security_Key::REGENERATE_SALT_NEXT_RUN_OPTION );
822 }
823
824 /**
825 * Automate the process of resolving, reverting, and ignoring security tweaks based on the provided data.
826 *
827 * @param array $data The data containing information about fixed, issues, and ignored security tweaks.
828 * @param string $request_reason The reason for the request, e.g., 'hub'.
829 *
830 * @return mixed Whether a reauthentication is needed after processing the tweaks.
831 * @since 2.8.1 Add $request_reason param. If there's a request from Hub the plugin doesn't send the error message.
832 */
833 public function automate( $data, $request_reason ) {
834 $this->refresh_tweaks_status();
835 $need_reauth = false;
836 // Resolve tweaks.
837 if ( ! empty( $data['fixed'] ) ) {
838 // There are some tweak that need manual apply, as files based, or change admin.
839 $manual_done = array(
840 'replace-admin-username',
841 'prevent-php-executed',
842 'wp-version',
843 'php-version',
844 'protect-information',
845 );
846 if ( 'hub' === $request_reason ) {
847 $manual_done[] = 'security-key';
848 }
849
850 $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done );
851 if ( ! empty( $diff_keys ) ) {
852 foreach ( $diff_keys as $slug ) {
853 $tweak = $this->get_tweak( $slug );
854 if ( $tweak->has_method( 'bulk_process' ) ) {
855 $ret = $tweak->bulk_process();
856 } else {
857 $ret = $tweak->process();
858 }
859
860 if ( is_wp_error( $ret ) ) {
861 if ( 'hub' === $request_reason ) {
862 continue;
863 }
864
865 return sprintf(
866 /* translators: 1: Security tweak title, 2: Error message */
867 esc_html__(
868 'There is an error while processing recommendation %1$s, error message: %2$s',
869 'defender-security'
870 ),
871 $tweak->get_label(),
872 $ret->get_error_message()
873 );
874 }
875
876 $this->model->mark( self::STATUS_RESOLVE, $slug );
877 }
878 if ( in_array( 'security-key', $diff_keys, true ) ) {
879 $need_reauth = true;
880 }
881 }
882 }
883 // Revert tweaks.
884 if ( ! empty( $data['issues'] ) ) {
885 $diff_keys = array_diff( $data['issues'], $this->model->issues );
886
887 if ( ! empty( $diff_keys ) ) {
888 // Issues.
889 foreach ( $diff_keys as $slug ) {
890 $tweak = $this->get_tweak( $slug );
891 $ret = $tweak->revert();
892 if ( is_wp_error( $ret ) ) {
893 if ( 'hub' === $request_reason ) {
894 continue;
895 }
896
897 return sprintf(
898 /* translators: 1: Security tweak title, 2: Error message */
899 esc_html__(
900 'There is an error while processing recommendation %1$s, error message: %2$s',
901 'defender-security'
902 ),
903 $tweak->get_label(),
904 $ret->get_error_message()
905 );
906 }
907 $this->model->mark( self::STATUS_ISSUES, $slug );
908 }
909 }
910 }
911 // Ignore tweaks.
912 if ( ! empty( $data['ignore'] ) ) {
913 $diff_keys = array_diff( $data['ignore'], $this->model->ignore );
914 if ( ! empty( $diff_keys ) ) {
915 foreach ( $diff_keys as $slug ) {
916 $this->model->mark( self::STATUS_IGNORE, $slug );
917 }
918 }
919 }
920
921 return $need_reauth;
922 }
923
924 /**
925 * Imports data into the model.
926 *
927 * @param array $data Data to be imported into the model.
928 */
929 public function import_data( array $data ) {
930 $enabled_user_enums = array();
931
932 if ( isset( $data['enabled_user_enums'] ) ) {
933 $enabled_user_enums = (array) $data['enabled_user_enums'];
934
935 unset( $data['enabled_user_enums'] );
936 }
937
938 $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums );
939
940 if ( ! empty( $data['security_key'] ) && is_array( $data['security_key'] ) ) {
941 $this->security_key->update_all_option( $data['security_key'] );
942 }
943
944 $model = new Model_Security_Tweaks();
945
946 $model->import( $data );
947 if ( $model->validate() ) {
948 $model->save();
949 }
950 }
951
952 /**
953 * Exports strings.
954 *
955 * @return array An array of strings.
956 */
957 public function export_strings(): array {
958 $this->refresh_tweaks_status();
959 $settings = new Model_Security_Tweaks();
960 $strings = array();
961 $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore );
962
963 if ( empty( $settings->issues ) ) {
964 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
965 } else {
966 $strings[] = sprintf(
967 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
968 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
969 count( $settings->fixed ),
970 $count_all
971 );
972 }
973
974 $tweak_notification = new Tweak_Reminder();
975 if ( 'enabled' === $tweak_notification->status ) {
976 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
977 }
978
979 return $strings;
980 }
981
982 /**
983 * Config strings.
984 *
985 * @param array $config Settings.
986 * @param bool $is_pro True if it is a pro version.
987 *
988 * @return array Strings to be displayed in the settings page.
989 */
990 public function config_strings( $config, $is_pro ): array {
991 $strings = array();
992 if ( empty( $config['issues'] ) ) {
993 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
994 } else {
995 $strings[] = sprintf(
996 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
997 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
998 is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0,
999 ( is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0 )
1000 + ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ? count( $config['issues'] ) : 0 )
1001 + ( is_array( $config['ignore'] ) || $config['ignore'] instanceof Countable ? count( $config['ignore'] ) : 0 )
1002 );
1003 }
1004 if ( 'enabled' === $config['notification'] ) {
1005 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1006 }
1007
1008 return $strings;
1009 }
1010
1011 /**
1012 * Update auto generate flag.
1013 *
1014 * @param Request $request Request object.
1015 *
1016 * @defender_route
1017 */
1018 public function update_autogenerate_flag( Request $request ): Response {
1019 $data = $request->get_data();
1020
1021 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
1022 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
1023 false;
1024
1025 $is_success = false;
1026 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1027
1028 if ( $this->security_key->set_is_autogenrate_keys( $is_autogen_flag ) ) {
1029 $is_success = true;
1030
1031 if ( $is_autogen_flag ) {
1032 $this->security_key->cron_schedule();
1033 $message = esc_html__( 'Security key/salt autogenerate enabled.', 'defender-security' );
1034 } else {
1035 $this->security_key->cron_unschedule();
1036 $message = esc_html__( 'Security key/salt autogenerate disabled.', 'defender-security' );
1037 }
1038 }
1039
1040 return new Response(
1041 $is_success,
1042 array( 'message' => $message )
1043 );
1044 }
1045
1046 /**
1047 * Get component security key instance.
1048 *
1049 * @return Security_Key
1050 */
1051 public function get_security_key() {
1052 return $this->security_key;
1053 }
1054
1055 /**
1056 * Update enabled user enums list.
1057 *
1058 * @param Request $request Request object.
1059 *
1060 * @return Response
1061 * @defender_route
1062 */
1063 public function update_enabled_user_enums( Request $request ): Response {
1064 $data = (array) $request->get_data();
1065 $enabled_user_enums = $data['enabled_user_enums'];
1066 $is_success = false;
1067 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1068
1069 if ( $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ) ) {
1070 $is_success = true;
1071 $message = esc_html__( 'User enumeration option(s) updated successfully.', 'defender-security' );
1072 }
1073
1074 return new Response(
1075 $is_success,
1076 array( 'message' => $message )
1077 );
1078 }
1079
1080 /**
1081 * Check XML-RPC status.
1082 *
1083 * @return Response
1084 * @defender_route
1085 */
1086 public function check_xml_rpc() {
1087 $tweak_xml_rpc = wd_di()->get( Disable_XML_RPC::class );
1088 // If this tweak is ON, block XML-RPC.
1089 if ( $tweak_xml_rpc->recheck() ) {
1090 $tweak_xml_rpc->add_hooks();
1091 }
1092
1093 return new Response( true, array() );
1094 }
1095
1096 /**
1097 * Tracks a tweak recommendation.
1098 *
1099 * @param string $title The title of the tweak recommendation.
1100 * @param string $status The status of the tweak recommendation.
1101 *
1102 * @return void
1103 */
1104 private function track_tweak( string $title, string $status ) {
1105 if ( ! defender_is_wp_cli() ) {
1106 $this->track_feature(
1107 'def_recommendation_applied',
1108 array(
1109 'Recommendation Name' => $title,
1110 'Status' => $status,
1111 )
1112 );
1113 }
1114 }
1115 }
1116