PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.0
Defender Security – Malware Scanner, Login Security & Firewall v5.6.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / controller / class-security-tweaks.php

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

1,115 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
273 $this->security_key->cron_unschedule();
274
275 $this->ajax_response( esc_html__( 'Security recommendation successfully ignored.', 'defender-security' ) );
276 }
277
278 /**
279 * Restore.
280 *
281 * @param Request $request Request object.
282 *
283 * @return Response|void
284 * @defender_route
285 */
286 public function restore( Request $request ) {
287 $data = $request->get_data(
288 array(
289 'slug' => array(
290 'type' => 'string',
291 'sanitize' => 'sanitize_text_field',
292 ),
293 )
294 );
295 $slug = $data['slug'] ?? false;
296 $tweak = $this->get_tweak( $slug );
297 if ( ! is_object( $tweak ) ) {
298 return new Response(
299 false,
300 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
301 );
302 }
303 $this->model->mark( self::STATUS_RESTORE, $slug );
304 // Track.
305 $this->track_tweak( $tweak->get_label(), 'Restored' );
306
307 if ( $this->security_key->get_is_autogenerate_keys() ) {
308 // Mandatory: cron_schedule method bypass scheduling if already a schedule for this job.
309 $this->security_key->cron_unschedule();
310 $this->security_key->cron_schedule();
311 }
312
313 $this->ajax_response( esc_html__( 'Security recommendation successfully restored.', 'defender-security' ) );
314 }
315
316 /**
317 * Recheck.
318 *
319 * @param Request $request Request object.
320 *
321 * @return Response
322 * @defender_route
323 */
324 public function recheck( Request $request ): Response {
325 $data = $request->get_data(
326 array(
327 'slug' => array(
328 'type' => 'string',
329 'sanitize' => 'sanitize_text_field',
330 ),
331 )
332 );
333 $slug = $data['slug'] ?? false;
334 $tweak = $this->get_tweak( $slug );
335
336 if ( ! is_object( $tweak ) ) {
337 return new Response(
338 false,
339 array(
340 'message' => esc_html__(
341 'The status cannot be verified as the request contains an invalid slug.',
342 'defender-security'
343 ),
344 )
345 );
346 }
347
348 $ret = $tweak->check();
349
350 if ( true === $ret ) {
351 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ), true, 1 );
352 }
353
354 if ( is_wp_error( $ret ) ) {
355 return new Response(
356 false,
357 array( 'message' => $ret->get_error_message() )
358 );
359 }
360
361 $msg = sprintf(
362 /* translators: %s: Tweak title. */
363 esc_html__( '%s is not actioned. Please ensure that all the instructions are followed.', 'defender-security' ),
364 $tweak->get_label() ?? ''
365 );
366
367 return new Response(
368 false,
369 array( 'message' => $msg )
370 );
371 }
372
373 /**
374 * Update security reminder.
375 *
376 * @param Request $request Request object.
377 *
378 * @return Response
379 * @defender_route
380 */
381 public function update_security_reminder( Request $request ): Response {
382 $data = $request->get_data();
383 $remind_date = $data['remind_date'] ?? false;
384
385 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
386 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
387 false;
388
389 if ( ! $remind_date ) {
390 return new Response(
391 false,
392 array( 'message' => esc_html__( 'Invalid Reminder frequency', 'defender-security' ) )
393 );
394 }
395
396 $values = array(
397 'reminder_duration' => $remind_date,
398 'reminder_date' => strtotime( '+' . $remind_date, time() ),
399 'is_autogenerate_keys' => $is_autogen_flag,
400 );
401
402 if ( update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values ) ) {
403
404 if ( true === $is_autogen_flag ) {
405 // Mandatory: cron_schedule method bypass scheduling if already a schedule for this job.
406 $this->security_key->cron_unschedule();
407 $this->security_key->cron_schedule();
408 }
409
410 return new Response(
411 true,
412 array( 'message' => esc_html__( 'Security recommendation successfully updated.', 'defender-security' ) )
413 );
414 } else {
415 return new Response(
416 false,
417 array( 'message' => esc_html__( 'Error while updating.', 'defender-security' ) )
418 );
419 }
420 }
421
422 /**
423 * AJAX Response handler.
424 *
425 * @param string $message The message to be displayed in the response.
426 * @param bool $is_success Whether the response should have a success status.
427 * @param bool|int $interval The cron interval for the security key autogeneration. Default is false.
428 *
429 * @return Response The AJAX response.
430 */
431 private function ajax_response( $message, $is_success = true, $interval = false ): Response {
432 global $wp_version;
433
434 $settings = new Model_Security_Tweaks();
435 $data = array(
436 'message' => $message,
437 'summary' => array(
438 'issues_count' => count( $settings->issues ),
439 'fixed_count' => count( $settings->fixed ),
440 'ignore_count' => count( $settings->ignore ),
441 'php_version' => PHP_VERSION,
442 'wp_version' => $wp_version,
443 ),
444 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
445 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
446 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
447 'issues_slugs' => $settings->issues,
448 );
449 if ( $interval ) {
450 $data['interval'] = $interval;
451 }
452
453 return new Response( $is_success, $data );
454 }
455
456 /**
457 * Enqueues scripts and styles for this page.
458 * Only enqueues assets if the page is active.
459 */
460 public function enqueue_assets() {
461 if ( ! $this->is_page_active() ) {
462 return;
463 }
464
465 wp_localize_script( 'def-securitytweaks', 'security_tweaks', $this->data_frontend() );
466 wp_enqueue_script( 'def-securitytweaks' );
467 $this->enqueue_main_assets();
468 }
469
470 /**
471 * Provides data for the dashboard widget.
472 *
473 * @return array
474 */
475 public function dashboard_widget(): array {
476 $tweak_arr = $this->model->get_tweak_types();
477 $data = array();
478 foreach ( $this->init_tweaks( self::STATUS_ISSUES ) as $slug => $tweak ) {
479 $data[] = array(
480 'title' => $tweak->get_label(),
481 'slug' => $slug,
482 );
483 }
484
485 return array(
486 'summary' => array(
487 'fixed_count' => $tweak_arr['count_fixed'],
488 'ignore_count' => $tweak_arr['count_ignored'],
489 'issues_count' => $tweak_arr['count_issues'],
490 ),
491 'issues' => $data,
492 );
493 }
494
495 /**
496 * Provides data for the frontend.
497 *
498 * @return array An array of data for the frontend.
499 */
500 public function data_frontend(): array {
501 $this->refresh_tweaks_status();
502 global $wp_version;
503
504 $not_allowed_bulk = array(
505 'php-version',
506 'replace-admin-username',
507 );
508 if ( 'nginx' === Server::get_current_server() ) {
509 $not_allowed_bulk[] = 'protect-information';
510 $not_allowed_bulk[] = 'prevent-php-executed';
511 }
512
513 $tweak_arr = $this->model->get_tweak_types();
514
515 $data = array(
516 'summary' => array(
517 'fixed_count' => $tweak_arr['count_fixed'],
518 'ignore_count' => $tweak_arr['count_ignored'],
519 'issues_count' => $tweak_arr['count_issues'],
520 'php_version' => PHP_VERSION,
521 'wp_version' => $wp_version,
522 ),
523 // Todo: improve the logic so that Tweak initialization happens in one go and return 3 different types.
524 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
525 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
526 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
527 'not_allowed_bulk' => $not_allowed_bulk,
528 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(),
529 'reminder_frequencies' => $this->security_key->reminder_frequencies(),
530 'enabled_user_enums' => $this->prevent_enum_users->get_enabled_user_enums(),
531 );
532
533 return array_merge( $data, $this->dump_routes_and_nonces() );
534 }
535
536 /**
537 * Renders the main view for this page.
538 */
539 public function main_view() {
540 $this->render( 'main' );
541 }
542
543 /**
544 * Bulk action for security tweaks.
545 * Handles bulk resolving, ignoring, or reverting security tweaks.
546 *
547 * @param Request $request The request object.
548 *
549 * @return Response
550 * @defender_route
551 */
552 public function bulk_action( Request $request ) {
553 $data = $request->get_data(
554 array(
555 'slugs' => array(
556 'type' => 'array',
557 'sanitize' => 'sanitize_text_field',
558 ),
559 'intention' => array(
560 'type' => 'string',
561 'sanitize' => 'sanitize_text_field',
562 ),
563 )
564 );
565 $slugs = $data['slugs'] ?? array();
566 $intention = $data['intention'] ?? false;
567 // Get processed and unprocessed tweaks.
568 [ $processed, $unprocessed ] = $this->security_tweaks_auto_action( $slugs, $intention );
569
570 $message = sprintf(
571 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
572 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
573 'ignore' === $intention ? 'ignored' : 'resolved',
574 $processed
575 );
576
577 if ( isset( $unprocessed ) && $unprocessed > 0 ) {
578 // If we have this case this mean the intention is resolved.
579 $message = sprintf(
580 /* translators: %d: Count security tweaks */
581 esc_html__(
582 '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.',
583 'defender-security'
584 ),
585 $processed
586 );
587
588 Config_Hub_Helper::set_clear_active_flag();
589 }
590
591 return $this->ajax_response( $message );
592 }
593
594 /**
595 * Mass processing.
596 *
597 * @param array $slugs Tweak slugs.
598 * @param string $intention Status.
599 *
600 * @return array
601 */
602 public function security_tweaks_auto_action( $slugs, $intention ): array {
603 $processed = 0;
604 $unprocessed = 0;
605
606 foreach ( $slugs as $slug ) {
607 $tweak = $this->get_tweak( $slug );
608 if ( 'ignore' === $intention ) {
609 $this->model->mark( self::STATUS_IGNORE, $slug );
610 // Track.
611 $this->track_tweak( $tweak->get_label(), 'Ignored' );
612 } elseif ( 'resolve' === $intention ) {
613 $wont_do = array(
614 'replace-admin-username',
615 'prevent-php-executed',
616 'wp-version',
617 'php-version',
618 'protect-information',
619 );
620 if ( in_array( $slug, $wont_do, true ) ) {
621 ++$unprocessed;
622 continue;
623 }
624 if ( $tweak->has_method( 'bulk_process' ) ) {
625 $ret = $tweak->bulk_process();
626 } else {
627 $ret = $tweak->process();
628 }
629 if ( is_wp_error( $ret ) ) {
630 $this->ajax_response(
631 sprintf(
632 /* translators: 1: Security tweak title, 2: Error message */
633 esc_html__(
634 'There is an error while processing recommendation %1$s, error message: %2$s',
635 'defender-security'
636 ),
637 $tweak->get_label(),
638 $ret->get_error_message()
639 ),
640 false
641 );
642 }
643 $this->model->mark( self::STATUS_RESOLVE, $slug );
644 // Track.
645 $this->track_tweak( $tweak->get_label(), 'Actioned' );
646 }
647 ++$processed;
648 }
649
650 return array( $processed, $unprocessed );
651 }
652
653 /**
654 * Refresh the tweak status and save their state.
655 *
656 * @return void
657 */
658 public function refresh_tweaks_status() {
659 $tweaks = $this->init_tweaks();
660 $settings = new Model_Security_Tweaks();
661 $fixed = array();
662 $issues = array();
663
664 foreach ( $tweaks as $slug => $class ) {
665 if ( $settings->is_tweak_ignore( $slug ) ) {
666 continue;
667 }
668
669 $is_resolved = $class->check();
670
671 if ( $is_resolved ) {
672 $fixed[] = $slug;
673 } else {
674 $issues[] = $slug;
675 }
676 }
677
678 $settings->fixed = $fixed;
679 $settings->issues = $issues;
680 $settings->save();
681 }
682
683 /**
684 * This function for shield every active tweaks up, we will use the cached result.
685 * No check function trigger in this init runtime.
686 */
687 private function boot() {
688 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
689 foreach ( $tweaks as $tweak ) {
690 $tweak->shield_up();
691 }
692 }
693
694 /**
695 * Instance all the tweaks, happen one time in init runtime.
696 *
697 * @param string $type Type of tweaks.
698 * @param string $format Object for internal use, array for frontend use.
699 *
700 * @return array
701 */
702 public function init_tweaks( $type = '', $format = 'object' ): array {
703 $classes = array(
704 Disable_XML_RPC::class,
705 WP_Version::class,
706 Hide_Error::class,
707 PHP_Version::class,
708 Change_Admin::class,
709 Security_Key::class,
710 Login_Duration::class,
711 Disable_Trackback::class,
712 Prevent_Enum_Users::class,
713 Disable_File_Editor::class,
714 );
715 if ( ! defender_is_wp_cli() ) {
716 // We don't load this in cli, as clearly no server is running.
717 $classes = array_merge(
718 $classes,
719 array(
720 Protect_Information::class,
721 Prevent_PHP::class,
722 )
723 );
724 }
725
726 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
727
728 if ( ! is_array( $tweaks ) ) {
729 foreach ( $classes as $class ) {
730 $obj = new $class();
731 $tweaks[ $obj->slug ] = $obj;
732 }
733 Array_Cache::set( 'tweaks', $tweaks, 'tweaks' );
734 }
735 $tmp = array();
736 if ( empty( $type ) ) {
737 $tmp = $tweaks;
738 } else {
739 $settings = new Model_Security_Tweaks();
740 $compare = $settings->$type;
741 foreach ( $compare as $slug ) {
742 if ( isset( $tweaks[ $slug ] ) ) {
743 $tmp[ $slug ] = $tweaks[ $slug ];
744 }
745 }
746 }
747
748 if ( 'array' === $format ) {
749 // We need to parse this as array.
750 foreach ( $tmp as $slug => $obj ) {
751 $arr = $obj->to_array();
752 $arr['status'] = $type;
753 $tmp[ $slug ] = $arr;
754 }
755 }
756
757 return $tmp;
758 }
759
760 /**
761 * Get tweak object from cache by slug.
762 *
763 * @param string $slug Tweak slug.
764 *
765 * @return mixed Tweak object if exists, else null.
766 */
767 private function get_tweak( $slug ) {
768 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
769
770 return $tweaks[ $slug ] ?? null;
771 }
772
773 /**
774 * Converts the current object state to an array.
775 *
776 * @return array The array representation of the object.
777 */
778 public function to_array(): array {
779 $this->refresh_tweaks_status();
780 $settings = new Model_Security_Tweaks();
781
782 return array(
783 'count' => array(
784 'issues' => count( $settings->issues ),
785 'resolved' => count( $settings->fixed ),
786 'total' => count( $this->init_tweaks() ),
787 ),
788 );
789 }
790
791 /**
792 * Removes settings for all submodules.
793 */
794 public function remove_settings() {
795 // Revert it first.
796 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
797 // Assign this so internal can use the current server.
798 $_POST['current_server'] = Server::get_current_server();
799 foreach ( $tweaks as $tweak ) {
800 $tweak->revert();
801
802 if ( method_exists( $tweak, 'delete_all_option' ) ) {
803 $tweak->delete_all_option();
804 }
805 }
806
807 ( new Model_Security_Tweaks() )->delete();
808
809 delete_site_transient( Server::CACHE_CURRENT_SERVER );
810 delete_site_transient( \WP_Defender\Component\Security_Tweaks\Servers\Apache::CACHE_APACHE_VERSION );
811 wp_clear_scheduled_hook( 'wpdef_sec_key_gen' );
812 }
813
814 /**
815 * Delete all the data & the cache.
816 */
817 public function remove_data() {
818 // Remove cached data.
819 Array_Cache::remove( 'tweaks', 'tweaks' );
820 delete_site_option( Security_Key::REGENERATE_SALT_NEXT_RUN_OPTION );
821 }
822
823 /**
824 * Automate the process of resolving, reverting, and ignoring security tweaks based on the provided data.
825 *
826 * @param array $data The data containing information about fixed, issues, and ignored security tweaks.
827 * @param string $request_reason The reason for the request, e.g., 'hub'.
828 *
829 * @return mixed Whether a reauthentication is needed after processing the tweaks.
830 * @since 2.8.1 Add $request_reason param. If there's a request from Hub the plugin doesn't send the error message.
831 */
832 public function automate( $data, $request_reason ) {
833 $this->refresh_tweaks_status();
834 $need_reauth = false;
835 // Resolve tweaks.
836 if ( ! empty( $data['fixed'] ) ) {
837 // There are some tweak that need manual apply, as files based, or change admin.
838 $manual_done = array(
839 'replace-admin-username',
840 'prevent-php-executed',
841 'wp-version',
842 'php-version',
843 'protect-information',
844 );
845 if ( 'hub' === $request_reason ) {
846 $manual_done[] = 'security-key';
847 }
848
849 $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done );
850 if ( ! empty( $diff_keys ) ) {
851 foreach ( $diff_keys as $slug ) {
852 $tweak = $this->get_tweak( $slug );
853 if ( $tweak->has_method( 'bulk_process' ) ) {
854 $ret = $tweak->bulk_process();
855 } else {
856 $ret = $tweak->process();
857 }
858
859 if ( is_wp_error( $ret ) ) {
860 if ( 'hub' === $request_reason ) {
861 continue;
862 }
863
864 return sprintf(
865 /* translators: 1: Security tweak title, 2: Error message */
866 esc_html__(
867 'There is an error while processing recommendation %1$s, error message: %2$s',
868 'defender-security'
869 ),
870 $tweak->get_label(),
871 $ret->get_error_message()
872 );
873 }
874
875 $this->model->mark( self::STATUS_RESOLVE, $slug );
876 }
877 if ( in_array( 'security-key', $diff_keys, true ) ) {
878 $need_reauth = true;
879 }
880 }
881 }
882 // Revert tweaks.
883 if ( ! empty( $data['issues'] ) ) {
884 $diff_keys = array_diff( $data['issues'], $this->model->issues );
885
886 if ( ! empty( $diff_keys ) ) {
887 // Issues.
888 foreach ( $diff_keys as $slug ) {
889 $tweak = $this->get_tweak( $slug );
890 $ret = $tweak->revert();
891 if ( is_wp_error( $ret ) ) {
892 if ( 'hub' === $request_reason ) {
893 continue;
894 }
895
896 return sprintf(
897 /* translators: 1: Security tweak title, 2: Error message */
898 esc_html__(
899 'There is an error while processing recommendation %1$s, error message: %2$s',
900 'defender-security'
901 ),
902 $tweak->get_label(),
903 $ret->get_error_message()
904 );
905 }
906 $this->model->mark( self::STATUS_ISSUES, $slug );
907 }
908 }
909 }
910 // Ignore tweaks.
911 if ( ! empty( $data['ignore'] ) ) {
912 $diff_keys = array_diff( $data['ignore'], $this->model->ignore );
913 if ( ! empty( $diff_keys ) ) {
914 foreach ( $diff_keys as $slug ) {
915 $this->model->mark( self::STATUS_IGNORE, $slug );
916 }
917 }
918 }
919
920 return $need_reauth;
921 }
922
923 /**
924 * Imports data into the model.
925 *
926 * @param array $data Data to be imported into the model.
927 */
928 public function import_data( array $data ) {
929 $enabled_user_enums = array();
930
931 if ( isset( $data['enabled_user_enums'] ) ) {
932 $enabled_user_enums = (array) $data['enabled_user_enums'];
933
934 unset( $data['enabled_user_enums'] );
935 }
936
937 $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums );
938
939 if ( ! empty( $data['security_key'] ) && is_array( $data['security_key'] ) ) {
940 $this->security_key->update_all_option( $data['security_key'] );
941 }
942
943 $model = new Model_Security_Tweaks();
944
945 $model->import( $data );
946 if ( $model->validate() ) {
947 $model->save();
948 }
949 }
950
951 /**
952 * Exports strings.
953 *
954 * @return array An array of strings.
955 */
956 public function export_strings(): array {
957 $this->refresh_tweaks_status();
958 $settings = new Model_Security_Tweaks();
959 $strings = array();
960 $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore );
961
962 if ( empty( $settings->issues ) ) {
963 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
964 } else {
965 $strings[] = sprintf(
966 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
967 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
968 count( $settings->fixed ),
969 $count_all
970 );
971 }
972
973 $tweak_notification = new Tweak_Reminder();
974 if ( 'enabled' === $tweak_notification->status ) {
975 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
976 }
977
978 return $strings;
979 }
980
981 /**
982 * Config strings.
983 *
984 * @param array $config Settings.
985 * @param bool $is_pro True if it is a pro version.
986 *
987 * @return array Strings to be displayed in the settings page.
988 */
989 public function config_strings( $config, $is_pro ): array {
990 $strings = array();
991 if ( empty( $config['issues'] ) ) {
992 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
993 } else {
994 $strings[] = sprintf(
995 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
996 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
997 is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0,
998 ( is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0 )
999 + ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ? count( $config['issues'] ) : 0 )
1000 + ( is_array( $config['ignore'] ) || $config['ignore'] instanceof Countable ? count( $config['ignore'] ) : 0 )
1001 );
1002 }
1003 if ( 'enabled' === $config['notification'] ) {
1004 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1005 }
1006
1007 return $strings;
1008 }
1009
1010 /**
1011 * Update auto generate flag.
1012 *
1013 * @param Request $request Request object.
1014 *
1015 * @defender_route
1016 */
1017 public function update_autogenerate_flag( Request $request ): Response {
1018 $data = $request->get_data();
1019
1020 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
1021 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
1022 false;
1023
1024 $is_success = false;
1025 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1026
1027 if ( $this->security_key->set_is_autogenrate_keys( $is_autogen_flag ) ) {
1028 $is_success = true;
1029
1030 if ( $is_autogen_flag ) {
1031 $this->security_key->cron_schedule();
1032 $message = esc_html__( 'Security key/salt autogenerate enabled.', 'defender-security' );
1033 } else {
1034 $this->security_key->cron_unschedule();
1035 $message = esc_html__( 'Security key/salt autogenerate disabled.', 'defender-security' );
1036 }
1037 }
1038
1039 return new Response(
1040 $is_success,
1041 array( 'message' => $message )
1042 );
1043 }
1044
1045 /**
1046 * Get component security key instance.
1047 *
1048 * @return Security_Key
1049 */
1050 public function get_security_key() {
1051 return $this->security_key;
1052 }
1053
1054 /**
1055 * Update enabled user enums list.
1056 *
1057 * @param Request $request Request object.
1058 *
1059 * @return Response
1060 * @defender_route
1061 */
1062 public function update_enabled_user_enums( Request $request ): Response {
1063 $data = (array) $request->get_data();
1064 $enabled_user_enums = $data['enabled_user_enums'];
1065 $is_success = false;
1066 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1067
1068 if ( $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ) ) {
1069 $is_success = true;
1070 $message = esc_html__( 'User enumeration option(s) updated successfully.', 'defender-security' );
1071 }
1072
1073 return new Response(
1074 $is_success,
1075 array( 'message' => $message )
1076 );
1077 }
1078
1079 /**
1080 * Check XML-RPC status.
1081 *
1082 * @return Response
1083 * @defender_route
1084 */
1085 public function check_xml_rpc() {
1086 $tweak_xml_rpc = wd_di()->get( Disable_XML_RPC::class );
1087 // If this tweak is ON, block XML-RPC.
1088 if ( $tweak_xml_rpc->recheck() ) {
1089 $tweak_xml_rpc->add_hooks();
1090 }
1091
1092 return new Response( true, array() );
1093 }
1094
1095 /**
1096 * Tracks a tweak recommendation.
1097 *
1098 * @param string $title The title of the tweak recommendation.
1099 * @param string $status The status of the tweak recommendation.
1100 *
1101 * @return void
1102 */
1103 private function track_tweak( string $title, string $status ) {
1104 if ( ! defender_is_wp_cli() ) {
1105 $this->track_feature(
1106 'def_recommendation_applied',
1107 array(
1108 'Recommendation Name' => $title,
1109 'Status' => $status,
1110 )
1111 );
1112 }
1113 }
1114 }
1115