PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.0
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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.8.0, at src/controller/class-security-tweaks.php

1,122 lines 30.1 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 $test_err_reporting = defender_get_data_from_request( 'defender_test_error_reporting', 'g' );
106 if ( ! is_string( $test_err_reporting ) || '' === trim( $test_err_reporting ) ) {
107 return;
108 }
109 // It should be only trigger by admin.
110 if ( ! $this->check_permission() ) {
111 return;
112 }
113
114 $var = '$' . uniqid( '', true );
115 // This should output a warning. Ignored phpcs as it's a test.
116 echo ${$var}; // phpcs:ignore
117 exit();
118 }
119
120 /**
121 * Process.
122 *
123 * @param Request $request Request object.
124 *
125 * @return Response
126 * @defender_route
127 */
128 public function process( Request $request ) {
129 $data = $request->get_data(
130 array(
131 'slug' => array(
132 'type' => 'string',
133 'sanitize' => 'sanitize_text_field',
134 ),
135 'current_server' => array(
136 'type' => 'string',
137 'sanitize' => 'sanitize_text_field',
138 ),
139 )
140 );
141
142 $slug = $data['slug'] ?? false;
143 $tweak = $this->get_tweak( $slug );
144
145 if ( ! is_object( $tweak ) ) {
146 return new Response(
147 false,
148 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
149 );
150 }
151
152 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
153 $current_server = $data['current_server'] ?? false;
154 if ( ! $current_server ) {
155 return new Response(
156 false,
157 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
158 );
159 }
160
161 $ret = $tweak->process( $current_server );
162 } else {
163 $ret = $tweak->process();
164 }
165
166 if ( true === $ret ) {
167 Config_Hub_Helper::set_clear_active_flag();
168 $this->model->mark( self::STATUS_RESOLVE, $slug );
169 // Track.
170 $this->track_tweak( $tweak->get_label(), 'Actioned' );
171 // Response.
172 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) );
173 }
174 if ( is_wp_error( $ret ) ) {
175 $this->ajax_response( $ret->get_error_message(), false );
176 }
177
178 return new Response(
179 false,
180 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
181 );
182 }
183
184 /**
185 * Revert.
186 *
187 * @param Request $request Request object.
188 *
189 * @return Response
190 * @defender_route
191 */
192 public function revert( Request $request ) {
193 $data = $request->get_data(
194 array(
195 'slug' => array(
196 'type' => 'string',
197 'sanitize' => 'sanitize_text_field',
198 ),
199 'current_server' => array(
200 'type' => 'string',
201 'sanitize' => 'sanitize_text_field',
202 ),
203 )
204 );
205 $slug = $data['slug'] ?? false;
206 $tweak = $this->get_tweak( $slug );
207 $invalid = array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) );
208 if ( ! is_object( $tweak ) ) {
209 return new Response(
210 false,
211 $invalid
212 );
213 }
214 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
215 $current_server = $data['current_server'] ?? false;
216 if ( ! $current_server ) {
217 return new Response(
218 false,
219 $invalid
220 );
221 }
222 $ret = $tweak->revert( $current_server );
223 } else {
224 $ret = $tweak->revert();
225 }
226
227 if ( is_wp_error( $ret ) ) {
228 $this->ajax_response( $ret->get_error_message(), false );
229 }
230 if ( true === $ret ) {
231 Config_Hub_Helper::set_clear_active_flag();
232 $this->model->mark( self::STATUS_ISSUES, $slug );
233 // Track.
234 $this->track_tweak( $tweak->get_label(), 'Reverted' );
235 // Response.
236 $this->ajax_response( esc_html__( 'Security recommendation successfully reverted.', 'defender-security' ) );
237 }
238
239 return new Response(
240 false,
241 $invalid
242 );
243 }
244
245 /**
246 * Ignore.
247 *
248 * @param Request $request Request object.
249 *
250 * @return Response|void
251 * @defender_route
252 */
253 public function ignore( Request $request ) {
254 $data = $request->get_data(
255 array(
256 'slug' => array(
257 'type' => 'string',
258 'sanitize' => 'sanitize_text_field',
259 ),
260 )
261 );
262 $slug = $data['slug'] ?? false;
263 $tweak = $this->get_tweak( $slug );
264 if ( ! is_object( $tweak ) ) {
265 return new Response(
266 false,
267 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
268 );
269 }
270 $this->model->mark( self::STATUS_IGNORE, $slug );
271 // Track.
272 $this->track_tweak( $tweak->get_label(), 'Ignored' );
273 if ( 'security-key' === $slug ) {
274 $this->security_key->cron_unschedule();
275 }
276
277 $this->ajax_response( esc_html__( 'Security recommendation successfully ignored.', 'defender-security' ) );
278 }
279
280 /**
281 * Restore.
282 *
283 * @param Request $request Request object.
284 *
285 * @return Response|void
286 * @defender_route
287 */
288 public function restore( Request $request ) {
289 $data = $request->get_data(
290 array(
291 'slug' => array(
292 'type' => 'string',
293 'sanitize' => 'sanitize_text_field',
294 ),
295 )
296 );
297 $slug = $data['slug'] ?? false;
298 $tweak = $this->get_tweak( $slug );
299 if ( ! is_object( $tweak ) ) {
300 return new Response(
301 false,
302 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
303 );
304 }
305 $this->model->mark( self::STATUS_RESTORE, $slug );
306 // Track.
307 $this->track_tweak( $tweak->get_label(), 'Restored' );
308
309 if ( $this->security_key->get_is_autogenerate_keys() ) {
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 $this->security_key->cron_schedule();
406 } else {
407 $this->security_key->cron_unschedule();
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 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
532 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
533 );
534
535 return array_merge( $data, $this->dump_routes_and_nonces() );
536 }
537
538 /**
539 * Renders the main view for this page.
540 */
541 public function main_view() {
542 $this->render( 'main' );
543 }
544
545 /**
546 * Bulk action for security tweaks.
547 * Handles bulk resolving, ignoring, or reverting security tweaks.
548 *
549 * @param Request $request The request object.
550 *
551 * @return Response
552 * @defender_route
553 */
554 public function bulk_action( Request $request ) {
555 $data = $request->get_data(
556 array(
557 'slugs' => array(
558 'type' => 'array',
559 'sanitize' => 'sanitize_text_field',
560 ),
561 'intention' => array(
562 'type' => 'string',
563 'sanitize' => 'sanitize_text_field',
564 ),
565 )
566 );
567 $slugs = $data['slugs'] ?? array();
568 $intention = $data['intention'] ?? false;
569 // Get processed and unprocessed tweaks.
570 [ $processed, $unprocessed ] = $this->security_tweaks_auto_action( $slugs, $intention );
571
572 $message = sprintf(
573 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
574 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
575 'ignore' === $intention ? 'ignored' : 'resolved',
576 $processed
577 );
578
579 if ( isset( $unprocessed ) && $unprocessed > 0 ) {
580 // If we have this case this mean the intention is resolved.
581 $message = sprintf(
582 /* translators: %d: Count security tweaks */
583 esc_html__(
584 '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.',
585 'defender-security'
586 ),
587 $processed
588 );
589
590 Config_Hub_Helper::set_clear_active_flag();
591 }
592
593 return $this->ajax_response( $message );
594 }
595
596 /**
597 * Mass processing.
598 *
599 * @param array $slugs Tweak slugs.
600 * @param string $intention Status.
601 *
602 * @return array
603 */
604 public function security_tweaks_auto_action( $slugs, $intention ): array {
605 $processed = 0;
606 $unprocessed = 0;
607
608 foreach ( $slugs as $slug ) {
609 $tweak = $this->get_tweak( $slug );
610 if ( 'ignore' === $intention ) {
611 $this->model->mark( self::STATUS_IGNORE, $slug );
612 // Track.
613 $this->track_tweak( $tweak->get_label(), 'Ignored' );
614 } elseif ( 'resolve' === $intention ) {
615 $wont_do = array(
616 'replace-admin-username',
617 'prevent-php-executed',
618 'wp-version',
619 'php-version',
620 'protect-information',
621 );
622 if ( in_array( $slug, $wont_do, true ) ) {
623 ++$unprocessed;
624 continue;
625 }
626 if ( $tweak->has_method( 'bulk_process' ) ) {
627 $ret = $tweak->bulk_process();
628 } else {
629 $ret = $tweak->process();
630 }
631 if ( is_wp_error( $ret ) ) {
632 $this->ajax_response(
633 sprintf(
634 /* translators: 1: Security tweak title, 2: Error message */
635 esc_html__(
636 'There is an error while processing recommendation %1$s, error message: %2$s',
637 'defender-security'
638 ),
639 $tweak->get_label(),
640 $ret->get_error_message()
641 ),
642 false
643 );
644 }
645 $this->model->mark( self::STATUS_RESOLVE, $slug );
646 // Track.
647 $this->track_tweak( $tweak->get_label(), 'Actioned' );
648 }
649 ++$processed;
650 }
651
652 return array( $processed, $unprocessed );
653 }
654
655 /**
656 * Refresh the tweak status and save their state.
657 *
658 * @return void
659 */
660 public function refresh_tweaks_status() {
661 $tweaks = $this->init_tweaks();
662 $settings = new Model_Security_Tweaks();
663 $fixed = array();
664 $issues = array();
665
666 foreach ( $tweaks as $slug => $class ) {
667 if ( $settings->is_tweak_ignore( $slug ) ) {
668 continue;
669 }
670
671 $is_resolved = $class->check();
672
673 if ( $is_resolved ) {
674 $fixed[] = $slug;
675 } else {
676 $issues[] = $slug;
677 }
678 }
679
680 $settings->fixed = $fixed;
681 $settings->issues = $issues;
682 $settings->save();
683 }
684
685 /**
686 * This function for shield every active tweaks up, we will use the cached result.
687 * No check function trigger in this init runtime.
688 */
689 private function boot() {
690 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
691 foreach ( $tweaks as $tweak ) {
692 $tweak->shield_up();
693 }
694 }
695
696 /**
697 * Instance all the tweaks, happen one time in init runtime.
698 *
699 * @param string $type Type of tweaks.
700 * @param string $format Object for internal use, array for frontend use.
701 *
702 * @return array
703 */
704 public function init_tweaks( $type = '', $format = 'object' ): array {
705 $classes = array(
706 Disable_XML_RPC::class,
707 WP_Version::class,
708 Hide_Error::class,
709 PHP_Version::class,
710 Change_Admin::class,
711 Security_Key::class,
712 Login_Duration::class,
713 Disable_Trackback::class,
714 Prevent_Enum_Users::class,
715 Disable_File_Editor::class,
716 );
717 if ( ! defender_is_wp_cli() ) {
718 // We don't load this in cli, as clearly no server is running.
719 $classes = array_merge(
720 $classes,
721 array(
722 Protect_Information::class,
723 Prevent_PHP::class,
724 )
725 );
726 }
727
728 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
729
730 if ( ! is_array( $tweaks ) ) {
731 foreach ( $classes as $class ) {
732 $obj = new $class();
733 $tweaks[ $obj->slug ] = $obj;
734 }
735 Array_Cache::set( 'tweaks', $tweaks, 'tweaks' );
736 }
737 $tmp = array();
738 if ( ! is_string( $type ) || '' === trim( $type ) ) {
739 $tmp = $tweaks;
740 } else {
741 $settings = new Model_Security_Tweaks();
742 $compare = $settings->$type;
743 foreach ( $compare as $slug ) {
744 if ( isset( $tweaks[ $slug ] ) ) {
745 $tmp[ $slug ] = $tweaks[ $slug ];
746 }
747 }
748 }
749
750 if ( 'array' === $format ) {
751 // We need to parse this as array.
752 foreach ( $tmp as $slug => $obj ) {
753 $arr = $obj->to_array();
754 $arr['status'] = $type;
755 $tmp[ $slug ] = $arr;
756 }
757 }
758
759 return $tmp;
760 }
761
762 /**
763 * Get tweak object from cache by slug.
764 *
765 * @param string $slug Tweak slug.
766 *
767 * @return mixed Tweak object if exists, else null.
768 */
769 private function get_tweak( $slug ) {
770 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
771
772 return $tweaks[ $slug ] ?? null;
773 }
774
775 /**
776 * Converts the current object state to an array.
777 *
778 * @return array The array representation of the object.
779 */
780 public function to_array(): array {
781 $this->refresh_tweaks_status();
782 $settings = new Model_Security_Tweaks();
783
784 return array(
785 'count' => array(
786 'issues' => count( $settings->issues ),
787 'resolved' => count( $settings->fixed ),
788 'total' => count( $this->init_tweaks() ),
789 ),
790 );
791 }
792
793 /**
794 * Removes settings for all submodules.
795 */
796 public function remove_settings() {
797 // Revert it first.
798 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
799 // Assign this so internal can use the current server.
800 $_POST['current_server'] = Server::get_current_server();
801 foreach ( $tweaks as $tweak ) {
802 $tweak->revert();
803
804 if ( method_exists( $tweak, 'delete_all_option' ) ) {
805 $tweak->delete_all_option();
806 }
807 }
808
809 ( new Model_Security_Tweaks() )->delete();
810
811 delete_site_transient( Server::CACHE_CURRENT_SERVER );
812 delete_site_transient( \WP_Defender\Component\Security_Tweaks\Servers\Apache::CACHE_APACHE_VERSION );
813 $this->security_key->cron_unschedule();
814 }
815
816 /**
817 * Delete all the data & the cache.
818 */
819 public function remove_data() {
820 // Remove cached data.
821 Array_Cache::remove( 'tweaks', 'tweaks' );
822 delete_site_option( Security_Key::REGENERATE_SALT_NEXT_RUN_OPTION );
823 }
824
825 /**
826 * Automate the process of resolving, reverting, and ignoring security tweaks based on the provided data.
827 *
828 * @param array $data The data containing information about fixed, issues, and ignored security tweaks.
829 * @param string $request_reason The reason for the request, e.g., 'hub'.
830 *
831 * @return mixed Whether a reauthentication is needed after processing the tweaks.
832 * @since 2.8.1 Add $request_reason param. If there's a request from Hub the plugin doesn't send the error message.
833 */
834 public function automate( $data, $request_reason ) {
835 $this->refresh_tweaks_status();
836 $need_reauth = false;
837 // Resolve tweaks.
838 if ( isset( $data['fixed'] ) && is_array( $data['fixed'] ) && array() !== $data['fixed'] ) {
839 // There are some tweak that need manual apply, as files based, or change admin.
840 $manual_done = array(
841 'replace-admin-username',
842 'prevent-php-executed',
843 'wp-version',
844 'php-version',
845 'protect-information',
846 );
847 if ( 'hub' === $request_reason ) {
848 $manual_done[] = 'security-key';
849 }
850
851 $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done );
852 if ( array() !== $diff_keys ) {
853 foreach ( $diff_keys as $slug ) {
854 $tweak = $this->get_tweak( $slug );
855 if ( $tweak->has_method( 'bulk_process' ) ) {
856 $ret = $tweak->bulk_process();
857 } else {
858 $ret = $tweak->process();
859 }
860
861 if ( is_wp_error( $ret ) ) {
862 if ( 'hub' === $request_reason ) {
863 continue;
864 }
865
866 return sprintf(
867 /* translators: 1: Security tweak title, 2: Error message */
868 esc_html__(
869 'There is an error while processing recommendation %1$s, error message: %2$s',
870 'defender-security'
871 ),
872 $tweak->get_label(),
873 $ret->get_error_message()
874 );
875 }
876
877 $this->model->mark( self::STATUS_RESOLVE, $slug );
878 }
879 if ( in_array( 'security-key', $diff_keys, true ) ) {
880 $need_reauth = true;
881 }
882 }
883 }
884 // Revert tweaks.
885 if ( isset( $data['issues'] ) && is_array( $data['issues'] ) && array() !== $data['issues'] ) {
886 $diff_keys = array_diff( $data['issues'], $this->model->issues );
887
888 if ( array() !== $diff_keys ) {
889 // Issues.
890 foreach ( $diff_keys as $slug ) {
891 $tweak = $this->get_tweak( $slug );
892 $ret = $tweak->revert();
893 if ( is_wp_error( $ret ) ) {
894 if ( 'hub' === $request_reason ) {
895 continue;
896 }
897
898 return sprintf(
899 /* translators: 1: Security tweak title, 2: Error message */
900 esc_html__(
901 'There is an error while processing recommendation %1$s, error message: %2$s',
902 'defender-security'
903 ),
904 $tweak->get_label(),
905 $ret->get_error_message()
906 );
907 }
908 $this->model->mark( self::STATUS_ISSUES, $slug );
909 }
910 }
911 }
912 // Ignore tweaks.
913 if ( isset( $data['ignore'] ) && is_array( $data['ignore'] ) && array() !== $data['ignore'] ) {
914 $diff_keys = array_diff( $data['ignore'], $this->model->ignore );
915 if ( array() !== $diff_keys ) {
916 foreach ( $diff_keys as $slug ) {
917 $this->model->mark( self::STATUS_IGNORE, $slug );
918 }
919 }
920 }
921
922 return $need_reauth;
923 }
924
925 /**
926 * Imports data into the model.
927 *
928 * @param array $data Data to be imported into the model.
929 */
930 public function import_data( array $data ) {
931 $enabled_user_enums = array();
932
933 if ( isset( $data['enabled_user_enums'] ) ) {
934 $enabled_user_enums = (array) $data['enabled_user_enums'];
935
936 unset( $data['enabled_user_enums'] );
937 }
938
939 $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums );
940
941 if ( isset( $data['security_key'] ) && is_array( $data['security_key'] ) && array() !== $data['security_key'] ) {
942 $this->security_key->update_all_option( $data['security_key'] );
943 }
944
945 $model = new Model_Security_Tweaks();
946
947 $model->import( $data );
948 if ( $model->validate() ) {
949 $model->save();
950 }
951 }
952
953 /**
954 * Exports strings.
955 *
956 * @return array An array of strings.
957 */
958 public function export_strings(): array {
959 $this->refresh_tweaks_status();
960 $settings = new Model_Security_Tweaks();
961 $strings = array();
962 $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore );
963
964 if ( ! is_array( $settings->issues ) || array() === $settings->issues ) {
965 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
966 } else {
967 $strings[] = sprintf(
968 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
969 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
970 count( $settings->fixed ),
971 $count_all
972 );
973 }
974
975 $tweak_notification = new Tweak_Reminder();
976 if ( 'enabled' === $tweak_notification->status ) {
977 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
978 }
979
980 return $strings;
981 }
982
983 /**
984 * Config strings.
985 *
986 * @param array $config Settings.
987 * @param bool $is_pro True if it is a pro version.
988 *
989 * @return array Strings to be displayed in the settings page.
990 */
991 public function config_strings( $config, $is_pro ): array {
992 $strings = array();
993 if (
994 ! isset( $config['issues'] )
995 || ! ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable )
996 || 0 === count( $config['issues'] )
997 ) {
998 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
999 } else {
1000 $strings[] = sprintf(
1001 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
1002 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
1003 is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0,
1004 ( is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0 )
1005 + ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ? count( $config['issues'] ) : 0 )
1006 + ( is_array( $config['ignore'] ) || $config['ignore'] instanceof Countable ? count( $config['ignore'] ) : 0 )
1007 );
1008 }
1009 if ( 'enabled' === $config['notification'] ) {
1010 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1011 }
1012
1013 return $strings;
1014 }
1015
1016 /**
1017 * Update auto generate flag.
1018 *
1019 * @param Request $request Request object.
1020 *
1021 * @defender_route
1022 */
1023 public function update_autogenerate_flag( Request $request ): Response {
1024 $data = $request->get_data();
1025
1026 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
1027 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
1028 false;
1029
1030 $is_success = false;
1031 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1032
1033 if ( $this->security_key->set_is_autogenrate_keys( $is_autogen_flag ) ) {
1034 $is_success = true;
1035
1036 if ( $is_autogen_flag ) {
1037 $this->security_key->cron_schedule();
1038 $message = esc_html__( 'Security key/salt autogenerate enabled.', 'defender-security' );
1039 } else {
1040 $this->security_key->cron_unschedule();
1041 $message = esc_html__( 'Security key/salt autogenerate disabled.', 'defender-security' );
1042 }
1043 }
1044
1045 return new Response(
1046 $is_success,
1047 array( 'message' => $message )
1048 );
1049 }
1050
1051 /**
1052 * Get component security key instance.
1053 *
1054 * @return Security_Key
1055 */
1056 public function get_security_key() {
1057 return $this->security_key;
1058 }
1059
1060 /**
1061 * Update enabled user enums list.
1062 *
1063 * @param Request $request Request object.
1064 *
1065 * @return Response
1066 * @defender_route
1067 */
1068 public function update_enabled_user_enums( Request $request ): Response {
1069 $data = $request->get_data();
1070 $data = ! is_array( $data ) ? (array) $data : $data;
1071 $enabled_user_enums = $data['enabled_user_enums'];
1072 $is_success = false;
1073 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1074
1075 if ( $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ) ) {
1076 $is_success = true;
1077 $message = esc_html__( 'User enumeration option(s) updated successfully.', 'defender-security' );
1078 }
1079
1080 return new Response(
1081 $is_success,
1082 array( 'message' => $message )
1083 );
1084 }
1085
1086 /**
1087 * Check XML-RPC status.
1088 *
1089 * @return Response
1090 * @defender_route
1091 */
1092 public function check_xml_rpc() {
1093 $tweak_xml_rpc = wd_di()->get( Disable_XML_RPC::class );
1094 // If this tweak is ON, block XML-RPC.
1095 if ( $tweak_xml_rpc->recheck() ) {
1096 $tweak_xml_rpc->add_hooks();
1097 }
1098
1099 return new Response( true, array() );
1100 }
1101
1102 /**
1103 * Tracks a tweak recommendation.
1104 *
1105 * @param string $title The title of the tweak recommendation.
1106 * @param string $status The status of the tweak recommendation.
1107 *
1108 * @return void
1109 */
1110 private function track_tweak( string $title, string $status ) {
1111 if ( ! defender_is_wp_cli() ) {
1112 $this->track_feature(
1113 'def_recommendation_applied',
1114 array(
1115 'Recommendation Name' => $title,
1116 'Status' => $status,
1117 )
1118 );
1119 }
1120 }
1121 }
1122