PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/controller/class-security-tweaks.php +198 -48 5.9.0 → 6.3.0 View file →
@@ -8,12 +8,12 @@
8 8 namespace WP_Defender\Controller;
9 9
10 10 use Countable;
11 11 use WP_Defender\Event;
12 -use WP_Defender\Admin;
13 12 use Calotes\Component\Request;
14 13 use Calotes\Component\Response;
15 14 use Calotes\Helper\Array_Cache;
15 +use WP_Defender\Behavior\WPMUDEV;
16 16 use WP_Defender\Model\Notification\Tweak_Reminder;
17 17 use WP_Defender\Component\Config\Config_Hub_Helper;
18 18 use WP_Defender\Component\Security_Tweaks\Hide_Error;
19 19 use WP_Defender\Component\Security_Tweaks\WP_Version;
@@ -72,13 +72,33 @@
72 72
73 73 public const STATUS_ISSUES = 'issues', STATUS_RESOLVE = 'fixed', STATUS_IGNORE = 'ignore', STATUS_RESTORE = 'restore';
74 74
75 75 /**
76 + * Maps tweak slugs to frontend category identifiers.
77 + *
78 + * @var array<string,string>
79 + */
80 + private const TWEAK_CATEGORIES = array(
81 + 'disable-xml-rpc' => 'network',
82 + 'wp-version' => 'updates',
83 + 'hide-error' => 'error',
84 + 'php-version' => 'updates',
85 + 'replace-admin-username' => 'auth',
86 + 'security-key' => 'updates',
87 + 'login-duration' => 'auth',
88 + 'disable-trackback' => 'network',
89 + 'prevent-enum-users' => 'error',
90 + 'disable-file-editor' => 'access',
91 + 'protect-information' => 'error',
92 + 'prevent-php-executed' => 'access',
93 + );
94 +
95 + /**
76 96 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
77 97 */
78 98 public function __construct() {
79 99 $this->register_page(
80 - esc_html__( 'Recommendations', 'defender-security' ),
100 + esc_html__( 'Hardening', 'defender-security' ),
81 101 $this->slug,
82 102 array( $this, 'main_view' ),
83 103 $this->parent_slug
84 104 );
@@ -105,17 +125,16 @@
105 125 $test_err_reporting = defender_get_data_from_request( 'defender_test_error_reporting', 'g' );
106 126 if ( ! is_string( $test_err_reporting ) || '' === trim( $test_err_reporting ) ) {
107 127 return;
108 128 }
109 - // It should be only trigger by admin.
129 + // It should be triggered only by admin.
110 130 if ( ! $this->check_permission() ) {
111 131 return;
112 132 }
113 133
114 - $var = '$' . uniqid( '', true );
115 - // This should output a warning. Ignored phpcs as it's a test.
134 + // This should output a warning. Ignored by PHPCS as it's a test.
116 135 // @phpstan-ignore-next-line.
117 - echo ${$var}; // phpcs:ignore
136 + echo $undefined_variable; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentionally outputs an undefined variable to test warning handling.
118 137 exit();
119 138 }
120 139
121 140 /**
@@ -136,8 +155,12 @@
136 155 'current_server' => array(
137 156 'type' => 'string',
138 157 'sanitize' => 'sanitize_text_field',
139 158 ),
159 + 'username' => array(
160 + 'type' => 'string',
161 + 'sanitize' => 'sanitize_user',
162 + ),
140 163 )
141 164 );
142 165
143 166 $slug = $data['slug'] ?? false;
@@ -149,8 +172,39 @@
149 172 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
150 173 );
151 174 }
152 175
176 + if ( 'prevent-enum-users' === $slug ) {
177 + $pe_data = $request->get_data();
178 + $enabled_enums = $pe_data['enabled_user_enums'] ?? null;
179 + $all_enums = array( 'enum-rest', 'enum-oembed', 'enum-sitemap' );
180 + $this->prevent_enum_users->set_enabled_user_enums(
181 + is_array( $enabled_enums ) ? $enabled_enums : $all_enums
182 + );
183 + }
184 +
185 + if ( 'security-key' === $slug ) {
186 + $sk_data = $request->get_data();
187 + $remind_date = sanitize_text_field( $sk_data['remind_date'] ?? '' );
188 + if ( $remind_date ) {
189 + $is_autogen = filter_var( $sk_data['is_autogenerate_keys'] ?? false, FILTER_VALIDATE_BOOLEAN );
190 + $values = get_site_option( 'defender_security_tweaks_' . $this->security_key->slug, array() );
191 + $values['reminder_duration'] = $remind_date;
192 + $values['reminder_date'] = strtotime( '+' . $remind_date, time() );
193 + $values['is_autogenerate_keys'] = $is_autogen;
194 + update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values );
195 + if ( true === $is_autogen ) {
196 + $this->security_key->cron_schedule();
197 + } else {
198 + $this->security_key->cron_unschedule();
199 + }
200 + }
201 + }
202 +
203 + if ( 'replace-admin-username' === $slug && isset( $data['username'] ) && '' !== trim( $data['username'] ) ) {
204 + $_POST['username'] = $data['username'];
205 + }
206 +
153 207 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
154 208 $current_server = $data['current_server'] ?? false;
155 209 if ( ! $current_server ) {
156 210 return new Response(
@@ -171,8 +225,14 @@
171 225 $this->track_tweak( $tweak->get_label(), 'Actioned' );
172 226 // Response.
173 227 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) );
174 228 }
229 + if ( $ret instanceof Response && true === $ret->status ) {
230 + Config_Hub_Helper::set_clear_active_flag();
231 + $this->model->mark( self::STATUS_RESOLVE, $slug );
232 + $this->track_tweak( $tweak->get_label(), 'Actioned' );
233 + return $ret;
234 + }
175 235 if ( is_wp_error( $ret ) ) {
176 236 $this->ajax_response( $ret->get_error_message(), false );
177 237 }
178 238
@@ -420,8 +480,37 @@
420 480 }
421 481 }
422 482
423 483 /**
484 + * Returns tweaks for a given status formatted for the React frontend.
485 + *
486 + * Adds `category` and remaps the backend status string to the value the
487 + * frontend HARDENING_STATUS constants expect ('issue', 'resolved', 'ignored').
488 + *
489 + * @param string $status One of STATUS_ISSUES, STATUS_RESOLVE, STATUS_IGNORE.
490 + *
491 + * @return array
492 + */
493 + private function format_tweaks_for_frontend( string $status ): array {
494 + $status_map = array(
495 + self::STATUS_ISSUES => 'issue',
496 + self::STATUS_RESOLVE => 'resolved',
497 + self::STATUS_IGNORE => 'ignored',
498 + );
499 +
500 + $tweaks = $this->init_tweaks( $status, 'array' );
501 + $frontend_status = $status_map[ $status ] ?? $status;
502 +
503 + foreach ( $tweaks as &$tweak ) {
504 + $tweak['status'] = $frontend_status;
505 + $tweak['category'] = self::TWEAK_CATEGORIES[ $tweak['slug'] ] ?? 'access';
506 + }
507 + unset( $tweak );
508 +
509 + return $tweaks;
510 + }
511 +
512 + /**
424 513 * AJAX Response handler.
425 514 *
426 515 * @param string $message The message to be displayed in the response.
427 516 * @param bool $is_success Whether the response should have a success status.
@@ -431,22 +520,23 @@
431 520 */
432 521 private function ajax_response( $message, $is_success = true, $interval = false ): Response {
433 522 global $wp_version;
434 523
435 - $settings = new Model_Security_Tweaks();
436 - $data = array(
524 + // Creating a new Model_Security_Tweaks() here risks reading stale data from the
525 + // DB when the WP options cache hasn't flushed the write yet.
526 + $data = array(
437 527 'message' => $message,
438 528 'summary' => array(
439 - 'issues_count' => count( $settings->issues ),
440 - 'fixed_count' => count( $settings->fixed ),
441 - 'ignore_count' => count( $settings->ignore ),
529 + 'issues_count' => count( $this->model->issues ),
530 + 'fixed_count' => count( $this->model->fixed ),
531 + 'ignore_count' => count( $this->model->ignore ),
442 532 'php_version' => PHP_VERSION,
443 533 'wp_version' => $wp_version,
444 534 ),
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,
535 + 'issues' => $this->build_tweaks_response( $this->model->issues, 'issue' ),
536 + 'fixed' => $this->build_tweaks_response( $this->model->fixed, 'resolved' ),
537 + 'ignored' => $this->build_tweaks_response( $this->model->ignore, 'ignored' ),
538 + 'issues_slugs' => $this->model->issues,
449 539 );
450 540 if ( $interval ) {
451 541 $data['interval'] = $interval;
452 542 }
@@ -454,8 +544,36 @@
454 544 return new Response( $is_success, $data );
455 545 }
456 546
457 547 /**
548 + * Builds the frontend-shaped tweak array from a list of slugs.
549 + *
550 + * Uses the in-memory tweak object cache rather than re-querying the model,
551 + * so the result is always consistent with the state after mark() was called.
552 + *
553 + * @param array $slugs Slugs to include.
554 + * @param string $frontend_status Frontend status string ('issue', 'resolved', 'ignored').
555 + *
556 + * @return array
557 + */
558 + private function build_tweaks_response( array $slugs, string $frontend_status ): array {
559 + $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
560 + $result = array();
561 +
562 + foreach ( $slugs as $slug ) {
563 + if ( ! isset( $tweaks[ $slug ] ) ) {
564 + continue;
565 + }
566 + $arr = $tweaks[ $slug ]->to_array();
567 + $arr['status'] = $frontend_status;
568 + $arr['category'] = self::TWEAK_CATEGORIES[ $slug ] ?? 'access';
569 + $result[ $slug ] = $arr;
570 + }
571 +
572 + return $result;
573 + }
574 +
575 + /**
458 576 * Enqueues scripts and styles for this page.
459 577 * Only enqueues assets if the page is active.
460 578 */
461 579 public function enqueue_assets() {
@@ -462,10 +580,34 @@
462 580 if ( ! $this->is_page_active() ) {
463 581 return;
464 582 }
465 583
466 - wp_localize_script( 'def-securitytweaks', 'security_tweaks', $this->data_frontend() );
467 - wp_enqueue_script( 'def-securitytweaks' );
584 + $handle = 'defender-ui-hardening';
585 + wp_enqueue_script(
586 + $handle,
587 + WP_DEFENDER_BASE_URL . 'assets/js/hardening-ui.js',
588 + array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
589 + DEFENDER_VERSION,
590 + true
591 + );
592 + wp_set_script_translations( $handle, 'wpdef' );
593 +
594 + wp_localize_script(
595 + $handle,
596 + 'defenderUIData',
597 + array_merge(
598 + $this->get_shared_data(),
599 + $this->data_frontend()
600 + )
601 + );
602 +
603 + wp_enqueue_style(
604 + $handle,
605 + WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css',
606 + array(),
607 + DEFENDER_VERSION
608 + );
609 +
468 610 $this->enqueue_main_assets();
469 611 }
470 612
471 613 /**
@@ -512,9 +654,9 @@
512 654 }
513 655
514 656 $tweak_arr = $this->model->get_tweak_types();
515 657
516 - $data = array(
658 + $hardening_data = array(
517 659 'summary' => array(
518 660 'fixed_count' => $tweak_arr['count_fixed'],
519 661 'ignore_count' => $tweak_arr['count_ignored'],
520 662 'issues_count' => $tweak_arr['count_issues'],
@@ -520,21 +662,20 @@
520 662 'issues_count' => $tweak_arr['count_issues'],
521 663 'php_version' => PHP_VERSION,
522 664 'wp_version' => $wp_version,
523 665 ),
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' ),
666 + 'issues' => $this->format_tweaks_for_frontend( self::STATUS_ISSUES ),
667 + 'fixed' => $this->format_tweaks_for_frontend( self::STATUS_RESOLVE ),
668 + 'ignored' => $this->format_tweaks_for_frontend( self::STATUS_IGNORE ),
528 669 'not_allowed_bulk' => $not_allowed_bulk,
529 670 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(),
530 671 'reminder_frequencies' => $this->security_key->reminder_frequencies(),
531 672 '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 673 );
535 674
536 - return array_merge( $data, $this->dump_routes_and_nonces() );
675 + return array(
676 + 'hardening' => array_merge( $hardening_data, $this->dump_routes_and_nonces() ),
677 + );
537 678 }
538 679
539 680 /**
540 681 * Renders the main view for this page.
@@ -567,10 +708,14 @@
567 708 );
568 709 $slugs = $data['slugs'] ?? array();
569 710 $intention = $data['intention'] ?? false;
570 711 // Get processed and unprocessed tweaks.
571 - [ $processed, $unprocessed ] = $this->security_tweaks_auto_action( $slugs, $intention );
712 + [ $processed, $unprocessed, $error_message ] = $this->security_tweaks_auto_action( $slugs, $intention );
572 713
714 + if ( null !== $error_message ) {
715 + return $this->ajax_response( $error_message, false );
716 + }
717 +
573 718 $message = sprintf(
574 719 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
575 720 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
576 721 'ignore' === $intention ? 'ignored' : 'resolved',
@@ -602,10 +747,11 @@
602 747 *
603 748 * @return array
604 749 */
605 750 public function security_tweaks_auto_action( $slugs, $intention ): array {
606 - $processed = 0;
607 - $unprocessed = 0;
751 + $processed = 0;
752 + $unprocessed = 0;
753 + $error_message = null;
608 754
609 755 foreach ( $slugs as $slug ) {
610 756 $tweak = $this->get_tweak( $slug );
611 757 if ( 'ignore' === $intention ) {
@@ -629,29 +775,34 @@
629 775 } else {
630 776 $ret = $tweak->process();
631 777 }
632 778 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()
779 + $error_message = sprintf(
780 + /* translators: 1: Security tweak title, 2: Error message */
781 + esc_html__(
782 + 'There is an error while processing recommendation %1$s, error message: %2$s',
783 + 'defender-security'
642 784 ),
643 - false
785 + $tweak->get_label(),
786 + $ret->get_error_message()
644 787 );
788 + ++$unprocessed;
789 + // Don't mark a failed tweak as resolved; stop on first error (same as original behavior).
790 + break;
645 791 }
646 792 $this->model->mark( self::STATUS_RESOLVE, $slug );
647 793 // Track.
648 794 $this->track_tweak( $tweak->get_label(), 'Actioned' );
795 + } elseif ( 'restore' === $intention ) {
796 + // New bulk action since V6.
797 + $this->model->mark( self::STATUS_ISSUES, $slug );
798 + // Track.
799 + $this->track_tweak( $tweak->get_label(), 'Restored' );
649 800 }
650 801 ++$processed;
651 802 }
652 803
653 - return array( $processed, $unprocessed );
804 + return array( $processed, $unprocessed, $error_message );
654 805 }
655 806
656 807 /**
657 808 * Refresh the tweak status and save their state.
@@ -739,10 +890,15 @@
739 890 if ( ! is_string( $type ) || '' === trim( $type ) ) {
740 891 $tmp = $tweaks;
741 892 } else {
742 893 $settings = new Model_Security_Tweaks();
743 - // @phpstan-ignore-next-line
744 - $compare = $settings->$type;
894 +
895 + try {
896 + $compare = $settings->get_collection( $type );
897 + } catch ( \InvalidArgumentException $e ) {
898 + $compare = array();
899 + }
900 +
745 901 foreach ( $compare as $slug ) {
746 902 if ( isset( $tweaks[ $slug ] ) ) {
747 903 $tmp[ $slug ] = $tweaks[ $slug ];
748 904 }
@@ -973,13 +1129,8 @@
973 1129 $count_all
974 1130 );
975 1131 }
976 1132
977 - $tweak_notification = new Tweak_Reminder();
978 - if ( 'enabled' === $tweak_notification->status ) {
979 - $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
980 - }
981 -
982 1133 return $strings;
983 1134 }
984 1135
985 1136 /**
@@ -985,13 +1136,12 @@
985 1136 /**
986 1137 * Config strings.
987 1138 *
988 1139 * @param array $config Settings.
989 - * @param bool $is_pro True if it is a pro version.
990 1140 *
991 1141 * @return array Strings to be displayed in the settings page.
992 1142 */
993 - public function config_strings( $config, $is_pro ): array {
1143 + public function config_strings( $config ): array {
994 1144 $strings = array();
995 1145 if (
996 1146 ! isset( $config['issues'] )
997 1147 || ! ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable )