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/class-upgrader.php +498 -63 5.7.2 → 6.3.0 View file →
@@ -67,20 +67,20 @@
67 67 public function migrate_security_headers(): void {
68 68 $model = wd_di()->get( Security_Headers::class );
69 69 $option = get_site_option( $model->get_table() );
70 70
71 - if ( empty( $option ) ) {
71 + if ( null === $option || false === $option || '' === $option || '0' === $option || array() === $option ) {
72 72 // Part of Security tweaks data.
73 73 $old_key = 'wd_hardener_settings';
74 74 $old_settings = get_site_option( $old_key );
75 75 if ( ! is_array( $old_settings ) ) {
76 76 $old_settings = json_decode( $old_settings, true );
77 - if ( is_array( $old_settings ) && isset( $old_settings['data'] ) && ! empty( $old_settings['data'] ) ) {
77 + if ( is_array( $old_settings ) && isset( $old_settings['data'] ) && is_array( $old_settings['data'] ) && array() !== $old_settings['data'] ) {
78 78 // Exist 'X-Frame-Options'.
79 - if ( isset( $old_settings['data']['sh_xframe'] ) && ! empty( $old_settings['data']['sh_xframe'] ) ) {
79 + if ( isset( $old_settings['data']['sh_xframe'] ) && is_array( $old_settings['data']['sh_xframe'] ) && array() !== $old_settings['data']['sh_xframe'] ) {
80 80 $header_data = $old_settings['data']['sh_xframe'];
81 81
82 - $mode = ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) )
82 + $mode = ( isset( $header_data['mode'] ) && '' !== trim( $header_data['mode'] ) )
83 83 ? strtolower( $header_data['mode'] )
84 84 : false;
85 85 /**
86 86 * Directive ALLOW-FROM is deprecated. If header directive is ALLOW-FROM then set 'sameorigin'.
@@ -95,13 +95,17 @@
95 95 $model->sh_xframe = true;
96 96 }
97 97
98 98 // Exist 'X-XSS-Protection'.
99 - if ( isset( $old_settings['data']['sh_xss_protection'] ) && ! empty( $old_settings['data']['sh_xss_protection'] ) ) {
99 + if (
100 + isset( $old_settings['data']['sh_xss_protection'] )
101 + && is_array( $old_settings['data']['sh_xss_protection'] )
102 + && array() !== $old_settings['data']['sh_xss_protection']
103 + ) {
100 104 $header_data = $old_settings['data']['sh_xss_protection'];
101 105
102 106 if ( isset( $header_data['mode'] )
103 - && ! empty( $header_data['mode'] )
107 + && '' !== trim( $header_data['mode'] )
104 108 && in_array( $header_data['mode'], array( 'sanitize', 'block' ), true )
105 109 ) {
106 110 $model->sh_xss_protection_mode = $header_data['mode'];
107 111 $model->sh_xss_protection = true;
@@ -108,12 +112,16 @@
108 112 }
109 113 }
110 114
111 115 // Exist 'X-Content-Type-Options'.
112 - if ( isset( $old_settings['data']['sh_content_type_options'] ) && ! empty( $old_settings['data']['sh_content_type_options'] ) ) {
116 + if (
117 + isset( $old_settings['data']['sh_content_type_options'] )
118 + && is_array( $old_settings['data']['sh_content_type_options'] )
119 + && array() !== $old_settings['data']['sh_content_type_options']
120 + ) {
113 121 $header_data = $old_settings['data']['sh_content_type_options'];
114 122
115 - if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
123 + if ( isset( $header_data['mode'] ) && '' !== trim( $header_data['mode'] ) ) {
116 124 $model->sh_content_type_options_mode = $header_data['mode'];
117 125 $model->sh_content_type_options = true;
118 126 }
119 127 }
@@ -118,15 +126,27 @@
118 126 }
119 127 }
120 128
121 129 // Exist 'Strict Transport'.
122 - if ( isset( $old_settings['data']['sh_strict_transport'] ) && ! empty( $old_settings['data']['sh_strict_transport'] ) ) {
130 + if (
131 + isset( $old_settings['data']['sh_strict_transport'] )
132 + && is_array( $old_settings['data']['sh_strict_transport'] )
133 + && array() !== $old_settings['data']['sh_strict_transport']
134 + ) {
123 135 $header_data = $old_settings['data']['sh_strict_transport'];
124 136
125 - if ( isset( $header_data['hsts_preload'] ) && ! empty( $header_data['hsts_preload'] ) ) {
126 - $model->hsts_preload = (int) $header_data['hsts_preload'];
137 + $hsts_preload = isset( $header_data['hsts_preload'] ) ? (int) $header_data['hsts_preload'] : 0;
138 + if ( 0 < $hsts_preload ) {
139 + $model->hsts_preload = $hsts_preload;
127 140 }
128 - if ( isset( $header_data['include_subdomain'] ) && ! empty( $header_data['include_subdomain'] ) ) {
141 + if (
142 + isset( $header_data['include_subdomain'] )
143 + && ! is_null( $header_data['include_subdomain'] )
144 + && '' !== $header_data['include_subdomain']
145 + && '0' !== $header_data['include_subdomain']
146 + && false !== $header_data['include_subdomain']
147 + && ( ! is_int( $header_data['include_subdomain'] ) || 0 < $header_data['include_subdomain'] )
148 + ) {
129 149 $model->include_subdomain = in_array(
130 150 $header_data['include_subdomain'],
131 151 array( 'true', '1', 1 ),
132 152 true
@@ -131,19 +151,24 @@
131 151 array( 'true', '1', 1 ),
132 152 true
133 153 ) ? 1 : 0;
134 154 }
135 - if ( isset( $header_data['hsts_cache_duration'] ) && ! empty( $header_data['hsts_cache_duration'] ) ) {
136 - $model->hsts_cache_duration = $header_data['hsts_cache_duration'];
155 + $cache_duration = isset( $header_data['hsts_cache_duration'] ) ? trim( $header_data['hsts_cache_duration'] ) : '';
156 + if ( '' !== $cache_duration ) {
157 + $model->hsts_cache_duration = $cache_duration;
137 158 }
138 159 $model->sh_strict_transport = true;
139 160 }
140 161
141 162 // Exist 'Referrer Policy'.
142 - if ( isset( $old_settings['data']['sh_referrer_policy'] ) && ! empty( $old_settings['data']['sh_referrer_policy'] ) ) {
163 + if (
164 + isset( $old_settings['data']['sh_referrer_policy'] )
165 + && is_array( $old_settings['data']['sh_referrer_policy'] )
166 + && array() !== $old_settings['data']['sh_referrer_policy']
167 + ) {
143 168 $header_data = $old_settings['data']['sh_referrer_policy'];
144 169
145 - if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
170 + if ( isset( $header_data['mode'] ) && '' !== trim( $header_data['mode'] ) ) {
146 171 $model->sh_referrer_policy_mode = $header_data['mode'];
147 172 $model->sh_referrer_policy = true;
148 173 }
149 174 }
@@ -148,15 +173,26 @@
148 173 }
149 174 }
150 175
151 176 // Exist 'Feature-Policy'.
152 - if ( isset( $old_settings['data']['sh_feature_policy'] ) && ! empty( $old_settings['data']['sh_feature_policy'] ) ) {
177 + if (
178 + isset( $old_settings['data']['sh_feature_policy'] )
179 + && is_array( $old_settings['data']['sh_feature_policy'] )
180 + && array() !== $old_settings['data']['sh_feature_policy']
181 + ) {
153 182 $header_data = $old_settings['data']['sh_feature_policy'];
154 183
155 - if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
184 + if ( isset( $header_data['mode'] ) && '' !== trim( $header_data['mode'] ) ) {
156 185 $mode = strtolower( $header_data['mode'] );
157 186 $model->sh_feature_policy_mode = $mode;
158 - if ( 'origins' === $mode && isset( $header_data['values'] ) && ! empty( $header_data['values'] ) ) {
187 + if (
188 + 'origins' === $mode
189 + && isset( $header_data['values'] )
190 + && (
191 + ( is_array( $header_data['values'] ) && array() !== $header_data['values'] )
192 + || ( is_string( $header_data['values'] ) && '' !== trim( $header_data['values'] ) )
193 + )
194 + ) {
159 195 // The values differ from the values of the 'X-Frame-Options' key, because they may be an array.
160 196 if ( is_array( $header_data['values'] ) ) {
161 197 $model->sh_feature_policy_urls = implode( PHP_EOL, $header_data['values'] );
162 198 // Otherwise.
@@ -209,9 +245,9 @@
209 245 && version_compare( $current_version, '2.4', '<' )
210 246 ) {
211 247 $config_component = wd_di()->get( Backup_Settings::class );
212 248 $prev_data = $config_component->backup_data();
213 - if ( empty( $prev_data ) ) {
249 + if ( ! is_array( $prev_data ) || array() === $prev_data ) {
214 250 return;
215 251 }
216 252 $adapter = wd_di()->get( Config_Adapter::class );
217 253 $migrated_data = $adapter->upgrade( $prev_data );
@@ -219,9 +255,9 @@
219 255 // Hide Onboard page.
220 256 update_site_option( 'wp_defender_shown_activator', true );
221 257
222 258 $configs = $config_component->get_configs();
223 - if ( ! empty( $configs ) ) {
259 + if ( array() !== $configs ) {
224 260 foreach ( $configs as $k => $config ) {
225 261 if (
226 262 $config_component->verify_config_data( $config )
227 263 && ! $config_component->check_for_new_structure( $config['configs'] )
@@ -250,9 +286,9 @@
250 286 private function upgrade_2_4_2(): void {
251 287 // Update Scan settings.
252 288 $model_settings = wd_di()->get( Scan_Settings::class );
253 289 $option = get_site_option( $model_settings->get_table() );
254 - if ( ! empty( $option ) && ! is_array( $option ) ) {
290 + if ( is_string( $option ) && '' !== trim( $option ) ) {
255 291 $old_settings = json_decode( $option, true );
256 292 if ( is_array( $old_settings ) && isset( $old_settings['max_filesize'] ) ) {
257 293 $model_settings->filesize = (int) $old_settings['max_filesize'];
258 294 $model_settings->save();
@@ -259,9 +295,9 @@
259 295 }
260 296 }
261 297 // Update 'reminder_duration' value inside the Security Key tweak.
262 298 $old_settings = get_site_option( 'wd_hardener_settings' );
263 - if ( ! empty( $old_settings ) && ! is_array( $old_settings ) ) {
299 + if ( is_string( $old_settings ) && '' !== trim( $old_settings ) ) {
264 300 $old_settings = json_decode( $old_settings, true );
265 301 $tweak_sec_key = wd_di()->get( Security_Key::class );
266 302
267 303 if ( is_array( $old_settings ) && isset( $old_settings['data']['securityReminderDuration'] )
@@ -283,9 +319,9 @@
283 319 * @since 2.4.7
284 320 */
285 321 private function migrate_scan_integrity_check(): void {
286 322 $model = new Scan_Settings();
287 - $model->check_core = (bool) $model->integrity_check;
323 + $model->check_core = $model->integrity_check;
288 324 $model->save();
289 325 }
290 326
291 327 /**
@@ -300,9 +336,11 @@
300 336 return;
301 337 }
302 338
303 339 $db_version = get_site_option( 'wd_db_version' );
304 - if ( empty( $db_version ) ) {
340 + if ( ! is_string( $db_version ) || '' === $db_version ) {
341 + self::cache_event_time( 'plugin_installed' );
342 + $this->create_database_tables_common();
305 343 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
306 344 update_site_option( Feature_Modal::FEATURE_SLUG, true );
307 345
308 346 return;
@@ -308,11 +346,21 @@
308 346 return;
309 347 }
310 348
311 349 if ( DEFENDER_DB_VERSION === $db_version ) {
350 + // Check if we need to create pro tables (Free to Pro upgrade scenario).
351 + if ( class_exists( Bootstrap::class ) && method_exists( Bootstrap::class, 'create_database_tables' ) && false === (bool) get_site_option( 'wd_pro_tables_created', false ) ) {
352 + wd_di()->get( Bootstrap::class )->create_database_tables();
353 + }
312 354 return;
313 355 }
314 - $this->create_database_tables();
356 + // Cache last updated time.
357 + self::cache_event_time( 'plugin_upgraded' );
358 +
359 + $this->create_database_tables_common();
360 + if ( class_exists( Bootstrap::class ) && method_exists( Bootstrap::class, 'create_database_tables' ) ) {
361 + wd_di()->get( Bootstrap::class )->create_database_tables();
362 + }
315 363 $this->maybe_show_new_features( $db_version );
316 364 $this->migrate_configs( $db_version );
317 365
318 366 if ( version_compare( $db_version, '2.2.9', '<' ) ) {
@@ -429,8 +477,17 @@
429 477 }
430 478 if ( version_compare( $db_version, '5.7.0', '<' ) ) {
431 479 $this->upgrade_5_7_0();
432 480 }
481 + if ( version_compare( $db_version, '6.0.0', '<' ) ) {
482 + $this->upgrade_6_0_0();
483 + }
484 + if ( version_compare( $db_version, '6.1.0', '<' ) ) {
485 + $this->upgrade_6_1_0();
486 + }
487 + if ( version_compare( $db_version, '6.2.0', '<' ) ) {
488 + $this->upgrade_6_2_0();
489 + }
433 490 // This is not a new installation. Make a mark.
434 491 defender_no_fresh_install();
435 492 // Don't run any function below this line.
436 493 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
@@ -436,8 +493,27 @@
436 493 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
437 494 }
438 495
439 496 /**
497 + * Cache the event time.
498 + *
499 + * @param string $event Event key.
500 + */
501 + private static function cache_event_time( $event ) {
502 + $option_key = \WP_Defender\Helper\Analytics\Deactivation_Survey::EVENT_DATA_OPTION;
503 + $event_times = get_site_option( $option_key, array() );
504 + $event_times[ $event ] = time();
505 + update_site_option( $option_key, $event_times );
506 + }
507 +
508 + /**
509 + * Cache the last activated time.
510 + */
511 + public static function date_activated() {
512 + self::cache_event_time( 'plugin_activated' );
513 + }
514 +
515 + /**
440 516 * Index necessary columns.
441 517 * Sometimes this function call twice that's why we have to check index already exists or not.
442 518 * `dbDelta` not work on `ALTER TABLE` query, so we had to use $wpdb->query().
443 519 *
@@ -653,9 +729,9 @@
653 729 $nf_settings = new Notfound_Lockout();
654 730 $allowlist = $nf_settings->get_lockout_list( 'allowlist' );
655 731 $default_allowlist = array( '/cdn-cgi/challenge-platform/' );
656 732 $is_save = false;
657 - if ( ! empty( $allowlist ) ) {
733 + if ( array() !== $allowlist ) {
658 734 foreach ( $default_allowlist as $item ) {
659 735 if ( ! in_array( $item, $allowlist, true ) ) {
660 736 $allowlist[] = $item;
661 737 $is_save = true;
@@ -681,9 +757,11 @@
681 757 private function upgrade_2_5_6(): void {
682 758 $adapted_component = wd_di()->get( Legacy_Versions::class );
683 759 $issue_list = $adapted_component->get_scan_issue_data();
684 760 $ignored_list = $adapted_component->get_scan_ignored_data();
685 - if ( ! empty( $issue_list ) || ! empty( $ignored_list ) ) {
761 + if (
762 + ( is_array( $issue_list ) && array() !== $issue_list )
763 + || ( is_array( $ignored_list ) && array() !== $ignored_list ) ) {
686 764 $adapted_component->migrate_scan_data( $issue_list, $ignored_list );
687 765 $adapted_component->remove_old_scan_data( $issue_list, $ignored_list );
688 766 $adapted_component->change_onboarding_status();
689 767 }
@@ -702,18 +780,17 @@
702 780 *
703 781 * @return void
704 782 */
705 783 private function update_scan_error_send_body( $model ): void {
706 - if (
707 - isset( $model->configs['template']['error']['body'] )
708 - && ! empty( $model->configs['template']['error']['body'] )
709 - ) {
784 + $body = isset( $model->configs['template']['error']['body'] ) ? trim( $model->configs['template']['error']['body'] ) : '';
785 +
786 + if ( '' !== $body ) {
710 787 $needle = '{follow this link} and check the logs to see what casued the failure';
711 - if ( false !== stripos( $model->configs['template']['error']['body'], $needle ) ) {
788 + if ( false !== stripos( $body, $needle ) ) {
712 789 $model->configs['template']['error']['body'] = str_replace(
713 790 $needle,
714 791 'visit your site and run a manual scan',
715 - $model->configs['template']['error']['body']
792 + $body
716 793 );
717 794 $model->save();
718 795 }
719 796 }
@@ -728,9 +805,9 @@
728 805 private function upgrade_2_6_1(): void {
729 806 // Update the title of the basic config.
730 807 $config_component = wd_di()->get( Backup_Settings::class );
731 808 $configs = $config_component->get_configs();
732 - if ( ! empty( $configs ) ) {
809 + if ( array() !== $configs ) {
733 810 foreach ( $configs as $k => $config ) {
734 811 if ( 0 === strcmp( $config['name'], esc_html__( 'Basic config', 'defender-security' ) ) ) {
735 812 $config['name'] = esc_html__( 'Basic Config', 'defender-security' );
736 813
@@ -775,12 +852,13 @@
775 852 );
776 853
777 854 foreach ( $models as $model ) {
778 855 $is_updated = false;
779 - if ( ! empty( $model->configs['template']['error']['body'] ) ) {
780 - $subject = $this->replace_scan_email_content_error( $model->configs['template']['error']['body'] );
856 + $body = isset( $model->configs['template']['error']['body'] ) ? trim( $model->configs['template']['error']['body'] ) : '';
857 + if ( '' !== $body ) {
858 + $subject = $this->replace_scan_email_content_error( $body );
781 859
782 - if ( $model->configs['template']['error']['body'] !== $subject ) {
860 + if ( $body !== $subject ) {
783 861 $is_updated = true;
784 862
785 863 $model->configs['template']['error']['body'] = $subject;
786 864 }
@@ -785,12 +863,13 @@
785 863 $model->configs['template']['error']['body'] = $subject;
786 864 }
787 865 }
788 866
789 - if ( ! empty( $model->configs['template']['found']['body'] ) ) {
790 - $subject = $this->replace_scan_email_content_issue_found( $model->configs['template']['found']['body'] );
867 + $body = isset( $model->configs['template']['found']['body'] ) ? trim( $model->configs['template']['found']['body'] ) : '';
868 + if ( '' !== $body ) {
869 + $subject = $this->replace_scan_email_content_issue_found( $body );
791 870
792 - if ( $model->configs['template']['found']['body'] !== $subject ) {
871 + if ( $body !== $subject ) {
793 872 $is_updated = true;
794 873
795 874 $model->configs['template']['found']['body'] = $subject;
796 875 }
@@ -795,12 +874,13 @@
795 874 $model->configs['template']['found']['body'] = $subject;
796 875 }
797 876 }
798 877
799 - if ( ! empty( $model->configs['template']['not_found']['body'] ) ) {
800 - $subject = $this->replace_scan_email_content_issue_not_found( $model->configs['template']['not_found']['body'] );
878 + $body = isset( $model->configs['template']['not_found']['body'] ) ? trim( $model->configs['template']['not_found']['body'] ) : '';
879 + if ( '' !== $body ) {
880 + $subject = $this->replace_scan_email_content_issue_not_found( $body );
801 881
802 - if ( $model->configs['template']['not_found']['body'] !== $subject ) {
882 + if ( $body !== $subject ) {
803 883 $is_updated = true;
804 884
805 885 $model->configs['template']['not_found']['body'] = $subject;
806 886 }
@@ -863,13 +943,13 @@
863 943 wd_di()->get( Tweak_Reminder::class ),
864 944 );
865 945
866 946 foreach ( $models as $model ) {
867 - if ( ! empty( $model->in_house_recipients ) && is_array( $model->in_house_recipients ) ) {
947 + if ( is_array( $model->in_house_recipients ) && array() !== $model->in_house_recipients ) {
868 948 $is_updated = false;
869 949
870 950 foreach ( $model->in_house_recipients as &$recipient ) {
871 - if ( empty( $recipient['role'] ) ) {
951 + if ( ! isset( $recipient['role'] ) || ! is_string( $recipient['role'] ) || '' === trim( $recipient['role'] ) ) {
872 952 $is_updated = true;
873 953 $recipient['role'] = $this->get_current_user_role( $recipient['id'] );
874 954 }
875 955 }
@@ -992,9 +1072,9 @@
992 1072 */
993 1073 private function upgrade_3_2_0(): void {
994 1074 $model = wd_di()->get( Two_Fa_Settings::class );
995 1075
996 - if ( ! empty( $model->app_title ) ) {
1076 + if ( is_string( $model->app_title ) && '' !== $model->app_title ) {
997 1077 $model->app_title = wp_specialchars_decode( $model->app_title, ENT_QUOTES );
998 1078 }
999 1079
1000 1080 $model->custom_graphic_type = Two_Fa_Settings::CUSTOM_GRAPHIC_TYPE_UPLOAD;
@@ -1028,9 +1108,9 @@
1028 1108 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1029 1109 $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
1030 1110 ARRAY_A
1031 1111 );
1032 - while ( ! empty( $blogs ) && is_array( $blogs ) ) {
1112 + while ( is_array( $blogs ) && array() !== $blogs ) {
1033 1113 foreach ( $blogs as $blog ) {
1034 1114 switch_to_blog( $blog['blog_id'] );
1035 1115
1036 1116 $this->update_webauthn_user_handle_core( $service );
@@ -1066,9 +1146,9 @@
1066 1146
1067 1147 $users = get_users( array( 'fields' => array( 'ID', 'user_login', 'display_name' ) ) );
1068 1148
1069 1149 foreach ( $users as $user ) {
1070 - if ( empty( $data ) ) {
1150 + if ( ! is_array( $data ) || array() === $data ) {
1071 1151 break;
1072 1152 }
1073 1153
1074 1154 $user_credentials = array();
@@ -1092,9 +1172,9 @@
1092 1172 unset( $data[ $key ] );
1093 1173 }
1094 1174 }
1095 1175
1096 - if ( ! empty( $user_credentials ) ) {
1176 + if ( array() !== $user_credentials ) {
1097 1177 $service->setCredentials( (int) $user->ID, $user_credentials );
1098 1178 }
1099 1179 }
1100 1180 }
@@ -1154,9 +1234,9 @@
1154 1234 if ( $query->get_total() > 0 ) {
1155 1235 // Hashed codes.
1156 1236 foreach ( $query->get_results() as $user_id ) {
1157 1237 $backup_code = get_user_meta( $user_id, Fallback_Email::FALLBACK_BACKUP_CODE_KEY, true );
1158 - if ( ! empty( $backup_code ) && isset( $backup_code['code'], $backup_code['time'] ) ) {
1238 + if ( isset( $backup_code['code'], $backup_code['time'] ) ) {
1159 1239 update_user_meta(
1160 1240 $user_id,
1161 1241 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
1162 1242 array(
@@ -1308,9 +1388,8 @@
1308 1388 */
1309 1389 private function update_malware_notification_setting(): void {
1310 1390 $models = array(
1311 1391 wd_di()->get( Malware_Notification::class ),
1312 - wd_di()->get( Malware_Report::class ),
1313 1392 );
1314 1393
1315 1394 foreach ( $models as $model ) {
1316 1395 $is_updated = false;
@@ -1464,9 +1543,9 @@
1464 1543 if ( $prev_name !== $new_name ) {
1465 1544 $is_updated = true;
1466 1545 }
1467 1546 $prev_desc = $config['description'];
1468 - $new_desc = empty( $config['description'] ) ? '' : sanitize_textarea_field( $config['description'] );
1547 + $new_desc = ! is_string( $config['description'] ) || '' === trim( $config['description'] ) ? '' : sanitize_textarea_field( $config['description'] );
1469 1548 if ( $prev_desc !== $new_desc ) {
1470 1549 $is_updated = true;
1471 1550 }
1472 1551 }
@@ -1492,10 +1571,8 @@
1492 1571 // Changes for Lockout table.
1493 1572 $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_lockout MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1494 1573 // Changes for Lockout log table.
1495 1574 $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_lockout_log MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1496 - // Changes for Audit log table.
1497 - $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_audit_log MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1498 1575 $wpdb->show_errors( $prev_val );
1499 1576 }
1500 1577
1501 1578 /**
@@ -1505,9 +1582,9 @@
1505 1582 */
1506 1583 private function upgrade_3_11_0(): void {
1507 1584 // Move Global IP settings.
1508 1585 $option = get_site_option( wd_di()->get( Model_Blacklist_Lockout::class )->get_table() );
1509 - if ( ! empty( $option ) && is_string( $option ) ) {
1586 + if ( is_string( $option ) && '' !== trim( $option ) ) {
1510 1587 $old_settings = json_decode( $option, true );
1511 1588 if ( is_array( $old_settings ) && isset( $old_settings['global_ip_list'] ) ) {
1512 1589 $model_global_ip = wd_di()->get( Global_Ip_Lockout::class );
1513 1590 $model_global_ip->enabled = (bool) $old_settings['global_ip_list'];
@@ -1590,9 +1667,9 @@
1590 1667 $limit
1591 1668 ),
1592 1669 ARRAY_A
1593 1670 );
1594 - while ( ! empty( $blogs ) && is_array( $blogs ) ) {
1671 + while ( is_array( $blogs ) && array() !== $blogs ) {
1595 1672 foreach ( $blogs as $blog ) {
1596 1673 switch_to_blog( $blog['blog_id'] );
1597 1674
1598 1675 $maxmind_dir = $this->get_tmp_path() . DIRECTORY_SEPARATOR . MaxMind_Geolocation::DB_DIRECTORY;
@@ -1750,9 +1827,9 @@
1750 1827 */
1751 1828 private function update_ua_blocklist(): void {
1752 1829 $settings = wd_di()->get( User_Agent_Lockout::class );
1753 1830 $blacklist = $settings->get_lockout_list( 'blocklist', false );
1754 - if ( empty( $blacklist ) ) {
1831 + if ( array() === $blacklist ) {
1755 1832 return;
1756 1833 }
1757 1834 $blacklist = array_filter(
1758 1835 $blacklist,
@@ -1811,15 +1888,15 @@
1811 1888 */
1812 1889 private function improve_ua_blocklist(): void {
1813 1890 $settings = wd_di()->get( User_Agent_Lockout::class );
1814 1891 $blocklist_custom = $settings->get_lockout_list( 'blocklist' );
1815 - if ( empty( $blocklist_custom ) ) {
1892 + if ( array() === $blocklist_custom ) {
1816 1893 return;
1817 1894 }
1818 1895 // Get 'Blocklist Presets', check and remove duplicates on 'Custom User Agents'.
1819 1896 $blocklist_presets = User_Agent_Service::get_nested_keys_of_blocklist_presets();
1820 1897 $common_result = array_intersect( $blocklist_custom, $blocklist_presets );
1821 - if ( ! empty( $common_result ) ) {
1898 + if ( array() !== $common_result ) {
1822 1899 $blocklist_custom = User_Agent_Service::check_and_remove_duplicates(
1823 1900 $blocklist_custom,
1824 1901 $common_result
1825 1902 );
@@ -1831,9 +1908,9 @@
1831 1908 }
1832 1909 // The same, but for 'Script Presets'.
1833 1910 $script_presets = array_keys( User_Agent_Service::get_script_presets() );
1834 1911 $common_result = array_intersect( $blocklist_custom, $script_presets );
1835 - if ( ! empty( $common_result ) ) {
1912 + if ( array() !== $common_result ) {
1836 1913 $blocklist_custom = User_Agent_Service::check_and_remove_duplicates(
1837 1914 $blocklist_custom,
1838 1915 $common_result
1839 1916 );
@@ -1919,8 +1996,366 @@
1919 1996 // Add the "What's new" modal.
1920 1997 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1921 1998 // Add Cloudflare challenge-platform to 404 whitelist.
1922 1999 $this->force_nf_lockout_exclusions();
1923 - // Remove the prev Breadcrumbs.
1924 - wd_di()->get( \WP_Defender\Component\Breadcrumbs::class )->delete_previous_meta();
2000 + }
2001 +
2002 + /**
2003 + * Migrates the uninstall settings from v5.11 to v6.0.
2004 + *
2005 + * During the upgrade:
2006 + * - If at least one of the three options is set to Preserve/Keep,
2007 + * all three options are set to Preserve/Keep.
2008 + */
2009 + private function combine_all_preserve_settings_to_one() {
2010 + $main_settings = wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class );
2011 +
2012 + if (
2013 + 'preserve' === $main_settings->uninstall_settings ||
2014 + 'keep' === $main_settings->uninstall_data ||
2015 + 'keep' === $main_settings->uninstall_quarantine
2016 + ) {
2017 + $main_settings->uninstall_settings = 'preserve';
2018 + $main_settings->uninstall_data = 'keep';
2019 + $main_settings->uninstall_quarantine = 'keep';
2020 + $main_settings->save();
2021 + }
2022 + }
2023 +
2024 + /**
2025 + * Since with version 6.0.0, two nested options:
2026 + * - Scan core files
2027 + * - Scan plugin files,
2028 + * are hidden, and automatically take the value of the parent "File change detection" option.
2029 + */
2030 + private function combine_all_file_change_detection_settings_to_one() {
2031 + $scan_settings = wd_di()->get( Scan_Settings::class );
2032 + if ( $scan_settings->integrity_check ) {
2033 + $scan_settings->check_core = true;
2034 + $scan_settings->check_plugins = true;
2035 + } else {
2036 + $scan_settings->check_core = false;
2037 + $scan_settings->check_plugins = false;
2038 + }
2039 + $scan_settings->save();
2040 + }
2041 +
2042 + /**
2043 + * Upgrade to 6.0.0: Ensure quarantine table exists for Free HC users and other changes.
2044 + * The quarantine feature is now available in the free version.
2045 + *
2046 + * @return void
2047 + */
2048 + private function upgrade_6_0_0(): void {
2049 + $bootstrap = wd_di()->get( Bootstrap::class );
2050 + $bootstrap->create_table_quarantine();
2051 + $this->combine_all_preserve_settings_to_one();
2052 + $this->combine_all_file_change_detection_settings_to_one();
2053 + $this->update_issues_report_email_template();
2054 + $this->fix_scheduled_scanning_left_disabled();
2055 + $this->migrate_tweak_reminder_schedule();
2056 + $this->migrate_two_fa_force_auth_roles();
2057 + // Add the "What's new" modal only when landing on the exact release it belongs to,
2058 + // so it does not persist into later patch/minor releases (e.g. 6.0.1, 6.1).
2059 + Feature_Modal::maybe_enable_welcome_modal();
2060 + }
2061 +
2062 + /**
2063 + * Upgrade to 6.1.0: Fix Tweak Reminder est_timestamp left as 0 by the 6.0.0 migration.
2064 + * Also sync all report schedules to match Malware Report.
2065 + *
2066 + * @return void
2067 + */
2068 + private function upgrade_6_1_0(): void {
2069 + $model = wd_di()->get( Tweak_Reminder::class );
2070 + if ( 'report' === $model->type && (int) $model->est_timestamp <= 0 ) {
2071 + // Calling save() recovers the correct state: last_sent is
2072 + // reset to now if it is 0, then est_timestamp is recomputed via get_next_run().
2073 + $model->save();
2074 + }
2075 +
2076 + $this->sync_all_report_schedules();
2077 + }
2078 +
2079 + /**
2080 + * Upgrade to 6.2.0.
2081 + *
2082 + * @return void
2083 + */
2084 + private function upgrade_6_2_0(): void {
2085 + $this->fix_notification_recipients_format();
2086 + $this->cancel_recipients_of_inactive_reports();
2087 + }
2088 +
2089 + /**
2090 + * Unsubscribe every recipient of each inactive report so their stored preferences match
2091 + * the reports' last saved on/off state.
2092 + *
2093 + * @return void
2094 + */
2095 + private function cancel_recipients_of_inactive_reports(): void {
2096 + $report_classes = array(
2097 + Tweak_Reminder::class,
2098 + Malware_Report::class,
2099 + Firewall_Report::class,
2100 + Audit_Report::class,
2101 + );
2102 +
2103 + foreach ( $report_classes as $report_class ) {
2104 + $model = wd_di()->get( $report_class );
2105 + if ( $model->check_active_status() ) {
2106 + continue;
2107 + }
2108 +
2109 + $changed = false;
2110 + foreach ( array( 'in_house_recipients', 'out_house_recipients' ) as $group ) {
2111 + $recipients = 'in_house_recipients' === $group
2112 + ? $model->in_house_recipients
2113 + : $model->out_house_recipients;
2114 + if ( ! is_array( $recipients ) ) {
2115 + continue;
2116 + }
2117 +
2118 + foreach ( $recipients as $key => $recipient ) {
2119 + if ( Notification::USER_SUBSCRIBE_CANCELED !== ( $recipient['status'] ?? '' ) ) {
2120 + $recipients[ $key ]['status'] = Notification::USER_SUBSCRIBE_CANCELED;
2121 + $changed = true;
2122 + }
2123 + }
2124 +
2125 + if ( 'in_house_recipients' === $group ) {
2126 + $model->in_house_recipients = $recipients;
2127 + } else {
2128 + $model->out_house_recipients = $recipients;
2129 + }
2130 + }
2131 +
2132 + if ( $changed ) {
2133 + $model->save();
2134 + }
2135 + }
2136 + }
2137 +
2138 + /**
2139 + * Heal recipients that older 6.x versions stored as email-keyed objects instead of arrays,
2140 + * which breaks Defender 5 on downgrade.
2141 + *
2142 + * @return void
2143 + */
2144 + private function fix_notification_recipients_format(): void {
2145 + $tables = array(
2146 + wd_di()->get( Audit_Report::class )->get_table(),
2147 + wd_di()->get( Firewall_Notification::class )->get_table(),
2148 + wd_di()->get( Firewall_Report::class )->get_table(),
2149 + wd_di()->get( Malware_Notification::class )->get_table(),
2150 + wd_di()->get( Malware_Report::class )->get_table(),
2151 + wd_di()->get( Tweak_Reminder::class )->get_table(),
2152 + );
2153 +
2154 + foreach ( $tables as $table ) {
2155 + $raw = get_site_option( $table );
2156 + if ( false === $raw ) {
2157 + continue;
2158 + }
2159 +
2160 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
2161 + if ( ! is_array( $data ) ) {
2162 + continue;
2163 + }
2164 +
2165 + $changed = false;
2166 + foreach ( array( 'in_house_recipients', 'out_house_recipients', 'all_subscribers' ) as $key ) {
2167 + if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) ) {
2168 + // array_values() leaves a proper list untouched but reindexes an email-keyed one.
2169 + $reindexed = array_values( $data[ $key ] );
2170 + if ( $reindexed !== $data[ $key ] ) {
2171 + $data[ $key ] = $reindexed;
2172 + $changed = true;
2173 + }
2174 + }
2175 + }
2176 +
2177 + if ( $changed ) {
2178 + update_site_option( $table, wp_json_encode( $data ) );
2179 + }
2180 + }
2181 + }
2182 +
2183 + /**
2184 + * Sync all report schedules to match Malware Report.
2185 + *
2186 + * Since 6.0.0 the UI enforces a single shared schedule for all reports via sync_report_schedule.
2187 + * Sites upgrading from older versions may have each report on a different schedule.
2188 + * This migration copies Malware_Report's schedule to Scan_Settings (the UI master) and to
2189 + * all four report models, then recomputes est_timestamp on each so all report emails
2190 + * arrive at the same time.
2191 + *
2192 + * @return void
2193 + */
2194 + private function sync_all_report_schedules(): void {
2195 + // Get the schedule from Malware_Report.
2196 + $malware_report = wd_di()->get( Malware_Report::class );
2197 + $frequency = $malware_report->frequency;
2198 + $day = $malware_report->day;
2199 + $day_n = (int) $malware_report->day_n;
2200 + $time = $malware_report->time;
2201 +
2202 + // Update Scan_Settings so the schedule pill shows the correct label.
2203 + $scan_settings = wd_di()->get( Scan_Settings::class );
2204 + $scan_settings->frequency = $frequency;
2205 + $scan_settings->day = $day;
2206 + $scan_settings->day_n = $day_n;
2207 + $scan_settings->time = $time;
2208 + $scan_settings->save();
2209 +
2210 + // Apply the same schedule to all report models and recompute est_timestamp
2211 + // so every report fires at the same next occurrence.
2212 + $report_models = array(
2213 + $malware_report,
2214 + wd_di()->get( Firewall_Report::class ),
2215 + wd_di()->get( Audit_Report::class ),
2216 + wd_di()->get( Tweak_Reminder::class ),
2217 + );
2218 +
2219 + foreach ( $report_models as $model ) {
2220 + $model->frequency = $frequency;
2221 + $model->day = $day;
2222 + $model->day_n = $day_n;
2223 + $model->time = $time;
2224 +
2225 + $next_run = $model->get_next_run();
2226 + if ( $next_run instanceof \DateTime ) {
2227 + $model->est_timestamp = $next_run->getTimestamp();
2228 + }
2229 +
2230 + $model->save();
2231 + }
2232 +
2233 + // Show a one-time dashboard bubble so users know about the shared schedule.
2234 + update_site_option( \WP_Defender\Controller\Dashboard::REPORT_SCHEDULE_NOTICE_OPTION, true );
2235 + }
2236 +
2237 + /**
2238 + * Heal 'Scheduled Scanning' left disabled while its related report stayed active.
2239 + *
2240 + * The bundled "Basic Config" preset stored 'scheduled_scanning' out of sync with its
2241 + * 'report' flag, so applying it left scans disabled. Corrects the live setting and any
2242 + * already-stored config presets with the same mismatch.
2243 + *
2244 + * @return void
2245 + */
2246 + private function fix_scheduled_scanning_left_disabled(): void {
2247 + $scan_settings = wd_di()->get( Scan_Settings::class );
2248 + $malware_report = wd_di()->get( Malware_Report::class );
2249 + if ( ! $scan_settings->scheduled_scanning && Notification::STATUS_ACTIVE === $malware_report->status ) {
2250 + $scan_settings->scheduled_scanning = true;
2251 + $scan_settings->save();
2252 + }
2253 +
2254 + $service = wd_di()->get( Backup_Settings::class );
2255 + $configs = Config_Hub_Helper::get_configs( $service );
2256 +
2257 + foreach ( $configs as $key => $config ) {
2258 + $scan = $config['configs']['scan'] ?? null;
2259 + if ( ! is_array( $scan ) || ! isset( $scan['report'], $scan['scheduled_scanning'] ) ) {
2260 + continue;
2261 + }
2262 +
2263 + if ( 'enabled' === $scan['report'] && false === $scan['scheduled_scanning'] ) {
2264 + $configs[ $key ]['configs']['scan']['scheduled_scanning'] = true;
2265 + update_site_option( $key, $configs[ $key ] );
2266 + Config_Hub_Helper::update_on_hub( $configs[ $key ] );
2267 + }
2268 + }
2269 +
2270 + delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
2271 + }
2272 +
2273 + /**
2274 + * Clear force_auth_roles when user_roles is empty.
2275 + *
2276 + * @return void
2277 + */
2278 + private function migrate_two_fa_force_auth_roles(): void {
2279 + $model = wd_di()->get( Two_Fa_Settings::class );
2280 + if ( count( $model->user_roles ) === 0 ) {
2281 + $model->force_auth = false;
2282 + $model->force_auth_roles = array();
2283 + $model->save();
2284 + }
2285 + }
2286 +
2287 + /**
2288 + * Migrate the Hardening report (Tweak Reminder) schedule from 5.11 to 6.0.0.
2289 + *
2290 + * In 5.11, this report was saved with type 'notification' and its frequency was kept in
2291 + * `configs['reminder']`. In 6.0.0, it's saved with type 'report' and uses new, dedicated
2292 + * fields instead: `frequency`, `day`, `day_n`, `time`, `est_timestamp`.
2293 + *
2294 + * Without this migration, a site updated from 5.11 keeps the old type 'notification', so the
2295 + * new scheduling code can't calculate a valid next-send date and the report email gets sent
2296 + * on every cron run instead of on the configured schedule.
2297 + *
2298 + * @return void
2299 + */
2300 + private function migrate_tweak_reminder_schedule(): void {
2301 + $model = wd_di()->get( Tweak_Reminder::class );
2302 + if ( 'notification' !== $model->type ) {
2303 + return;
2304 + }
2305 +
2306 + $model->type = 'report';
2307 +
2308 + $valid_frequencies = array( 'daily', 'weekly', 'monthly' );
2309 + $reminder = $model->configs['reminder'] ?? '';
2310 + $model->frequency = in_array( $reminder, $valid_frequencies, true ) ? $reminder : 'weekly';
2311 +
2312 + if ( null === $model->day || '' === $model->day ) {
2313 + $model->day = 'sunday';
2314 + }
2315 + if ( null === $model->time || '' === $model->time ) {
2316 + $model->time = '4:00';
2317 + }
2318 + if ( ! is_int( $model->day_n ) || $model->day_n < 1 ) {
2319 + $model->day_n = 1;
2320 + }
2321 + // Compute est_timestamp.
2322 + $next_run = $model->get_next_run();
2323 + if ( $next_run instanceof \DateTime ) {
2324 + $model->est_timestamp = $next_run->getTimestamp();
2325 + }
2326 + // Clear the old notification style configs key.
2327 + unset( $model->configs['reminder'] );
2328 + $model->save();
2329 + }
2330 +
2331 + /**
2332 + * Update Issues Report email subject and body to match the new copy.
2333 + *
2334 + * @return void
2335 + */
2336 + private function update_issues_report_email_template(): void {
2337 + $models = array(
2338 + wd_di()->get( Malware_Notification::class ),
2339 + wd_di()->get( Malware_Report::class ),
2340 + );
2341 +
2342 + $default_subject = esc_html__( 'Issues Report for {SITE_URL}: {ISSUES_COUNT} issue(s) found.', 'defender-security' );
2343 + $default_body = esc_html__(
2344 + 'Hi {USER_NAME},
2345 +
2346 +A scan of {SITE_URL} identified {ISSUES_COUNT} issue(s). The issue(s) found is/are listed below.
2347 +
2348 +{ISSUES_LIST}',
2349 + 'defender-security'
2350 + );
2351 +
2352 + foreach ( $models as $model ) {
2353 + if ( ! isset( $model->configs['template']['found'] ) ) {
2354 + continue;
2355 + }
2356 + $model->configs['template']['found']['subject'] = $default_subject;
2357 + $model->configs['template']['found']['body'] = $default_body;
2358 + $model->save();
2359 + }
1925 2360 }
1926 2361 }