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

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