PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.0
Defender Security – Malware Scanner, Login Security & Firewall v5.7.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 / class-upgrader.php

class-upgrader.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.0, at src/class-upgrader.php

1,927 lines 60.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This file contains the logic for updating the WP Defender plugin.
4 *
5 * @package WP_Defender
6 */
7
8 namespace WP_Defender;
9
10 use wpdb;
11 use WP_User_Query;
12 use WP_Filesystem_Base;
13 use WP_Defender\Traits\IO;
14 use WP_Defender\Traits\User;
15 use WP_Defender\Component\Crypt;
16 use WP_Defender\Component\Firewall;
17 use WP_Defender\Component\Webauthn;
18 use WP_Defender\Model\Notification;
19 use Safe\Exceptions\SodiumException;
20 use WP_Defender\Component\Feature_Modal;
21 use WP_Defender\Controller\Data_Tracking;
22 use WP_Defender\Component\Backup_Settings;
23 use WP_Defender\Traits\Defender_Bootstrap;
24 use WP_Defender\Component\Legacy_Versions;
25 use WP_Defender\Controller\Security_Tweaks;
26 use WP_Defender\Model\Setting\Security_Headers;
27 use WP_Defender\Model\Setting\Notfound_Lockout;
28 use WP_Defender\Model\Notification\Audit_Report;
29 use WP_Defender\Model\Setting\Global_Ip_Lockout;
30 use WP_Defender\Component\Config\Config_Adapter;
31 use WP_Defender\Model\Setting\User_Agent_Lockout;
32 use WP_Defender\Integrations\MaxMind_Geolocation;
33 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
34 use WP_Defender\Model\Notification\Malware_Report;
35 use WP_Defender\Model\Notification\Tweak_Reminder;
36 use WP_Defender\Component\Config\Config_Hub_Helper;
37 use WP_Defender\Model\Notification\Firewall_Report;
38 use WP_Defender\Model\Setting\Scan as Scan_Settings;
39 use WP_Defender\Component\Two_Fa as Two_Fa_Component;
40 use WP_Defender\Component\Two_Factor\Providers\Totp;
41 use WP_Defender\Model\Setting\Two_Fa as Two_Fa_Settings;
42 use WP_Defender\Component\Security_Tweaks\Security_Key;
43 use WP_Defender\Model\Notification\Malware_Notification;
44 use WP_Defender\Model\Notification\Firewall_Notification;
45 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
46 use WP_Defender\Helper\Analytics\Firewall as Firewall_Analytics;
47 use WP_Defender\Model\Setting\Blacklist_Lockout as Model_Blacklist_Lockout;
48 use WP_Defender\Model\Setting\Firewall as Model_Firewall;
49 use WP_Defender\Component\User_Agent as User_Agent_Service;
50 use function WP_Filesystem;
51
52 /**
53 * Upgrade the WP Defender plugin to the latest version.
54 */
55 class Upgrader {
56
57 use User;
58 use Webauthn_Trait;
59 use IO;
60 use Defender_Bootstrap;
61
62 /**
63 * Migrate old security headers from security tweaks. Trigger it once time.
64 *
65 * @return void
66 */
67 public function migrate_security_headers(): void {
68 $model = wd_di()->get( Security_Headers::class );
69 $option = get_site_option( $model->get_table() );
70
71 if ( empty( $option ) ) {
72 // Part of Security tweaks data.
73 $old_key = 'wd_hardener_settings';
74 $old_settings = get_site_option( $old_key );
75 if ( ! is_array( $old_settings ) ) {
76 $old_settings = json_decode( $old_settings, true );
77 if ( is_array( $old_settings ) && isset( $old_settings['data'] ) && ! empty( $old_settings['data'] ) ) {
78 // Exist 'X-Frame-Options'.
79 if ( isset( $old_settings['data']['sh_xframe'] ) && ! empty( $old_settings['data']['sh_xframe'] ) ) {
80 $header_data = $old_settings['data']['sh_xframe'];
81
82 $mode = ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) )
83 ? strtolower( $header_data['mode'] )
84 : false;
85 /**
86 * Directive ALLOW-FROM is deprecated. If header directive is ALLOW-FROM then set 'sameorigin'.
87 *
88 * @since 2.5.0
89 */
90 if ( 'allow-from' === $mode ) {
91 $model->sh_xframe_mode = 'sameorigin';
92 } elseif ( in_array( $mode, array( 'sameorigin', 'deny' ), true ) ) {
93 $model->sh_xframe_mode = $mode;
94 }
95 $model->sh_xframe = true;
96 }
97
98 // Exist 'X-XSS-Protection'.
99 if ( isset( $old_settings['data']['sh_xss_protection'] ) && ! empty( $old_settings['data']['sh_xss_protection'] ) ) {
100 $header_data = $old_settings['data']['sh_xss_protection'];
101
102 if ( isset( $header_data['mode'] )
103 && ! empty( $header_data['mode'] )
104 && in_array( $header_data['mode'], array( 'sanitize', 'block' ), true )
105 ) {
106 $model->sh_xss_protection_mode = $header_data['mode'];
107 $model->sh_xss_protection = true;
108 }
109 }
110
111 // Exist 'X-Content-Type-Options'.
112 if ( isset( $old_settings['data']['sh_content_type_options'] ) && ! empty( $old_settings['data']['sh_content_type_options'] ) ) {
113 $header_data = $old_settings['data']['sh_content_type_options'];
114
115 if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
116 $model->sh_content_type_options_mode = $header_data['mode'];
117 $model->sh_content_type_options = true;
118 }
119 }
120
121 // Exist 'Strict Transport'.
122 if ( isset( $old_settings['data']['sh_strict_transport'] ) && ! empty( $old_settings['data']['sh_strict_transport'] ) ) {
123 $header_data = $old_settings['data']['sh_strict_transport'];
124
125 if ( isset( $header_data['hsts_preload'] ) && ! empty( $header_data['hsts_preload'] ) ) {
126 $model->hsts_preload = (int) $header_data['hsts_preload'];
127 }
128 if ( isset( $header_data['include_subdomain'] ) && ! empty( $header_data['include_subdomain'] ) ) {
129 $model->include_subdomain = in_array(
130 $header_data['include_subdomain'],
131 array( 'true', '1', 1 ),
132 true
133 ) ? 1 : 0;
134 }
135 if ( isset( $header_data['hsts_cache_duration'] ) && ! empty( $header_data['hsts_cache_duration'] ) ) {
136 $model->hsts_cache_duration = $header_data['hsts_cache_duration'];
137 }
138 $model->sh_strict_transport = true;
139 }
140
141 // Exist 'Referrer Policy'.
142 if ( isset( $old_settings['data']['sh_referrer_policy'] ) && ! empty( $old_settings['data']['sh_referrer_policy'] ) ) {
143 $header_data = $old_settings['data']['sh_referrer_policy'];
144
145 if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
146 $model->sh_referrer_policy_mode = $header_data['mode'];
147 $model->sh_referrer_policy = true;
148 }
149 }
150
151 // Exist 'Feature-Policy'.
152 if ( isset( $old_settings['data']['sh_feature_policy'] ) && ! empty( $old_settings['data']['sh_feature_policy'] ) ) {
153 $header_data = $old_settings['data']['sh_feature_policy'];
154
155 if ( isset( $header_data['mode'] ) && ! empty( $header_data['mode'] ) ) {
156 $mode = strtolower( $header_data['mode'] );
157 $model->sh_feature_policy_mode = $mode;
158 if ( 'origins' === $mode && isset( $header_data['values'] ) && ! empty( $header_data['values'] ) ) {
159 // The values differ from the values of the 'X-Frame-Options' key, because they may be an array.
160 if ( is_array( $header_data['values'] ) ) {
161 $model->sh_feature_policy_urls = implode( PHP_EOL, $header_data['values'] );
162 // Otherwise.
163 } elseif ( is_string( $header_data['values'] ) ) {
164 $urls = explode( ' ', $header_data['values'] );
165 $model->sh_feature_policy_urls = implode( PHP_EOL, $urls );
166 }
167 }
168 $model->sh_feature_policy = true;
169 }
170 }
171 // Save.
172 $model->save();
173 }
174 }
175 }
176 }
177
178 /**
179 * If user upgrade from an older version to the latest version.
180 *
181 * @param mixed $current_version The current version.
182 *
183 * @return void
184 */
185 public function maybe_show_new_features( $current_version ) {
186 if ( false === $current_version ) {
187 // Do nothing.
188 return;
189 }
190
191 // Set the version where we have added the new feature.
192 // Update it when you want to show modal on a specific version.
193 if ( version_compare( $current_version, '2.4', '<' ) ) {
194 update_site_option( 'wd_show_new_feature', true );
195 }
196 }
197
198 /**
199 * Migrate configs for latest versions.
200 *
201 * @param mixed $current_version The current version.
202 *
203 * @return void
204 * @since 2.4
205 */
206 public function migrate_configs( $current_version ) {
207 if (
208 version_compare( $current_version, '2.2', '>=' )
209 && version_compare( $current_version, '2.4', '<' )
210 ) {
211 $config_component = wd_di()->get( Backup_Settings::class );
212 $prev_data = $config_component->backup_data();
213 if ( empty( $prev_data ) ) {
214 return;
215 }
216 $adapter = wd_di()->get( Config_Adapter::class );
217 $migrated_data = $adapter->upgrade( $prev_data );
218 $config_component->restore_data( $migrated_data, 'migration' );
219 // Hide Onboard page.
220 update_site_option( 'wp_defender_shown_activator', true );
221
222 $configs = $config_component->get_configs();
223 if ( ! empty( $configs ) ) {
224 foreach ( $configs as $k => $config ) {
225 if (
226 $config_component->verify_config_data( $config )
227 && ! $config_component->check_for_new_structure( $config['configs'] )
228 ) {
229 $new_data = $config;
230 $new_data['configs'] = $adapter->upgrade( $config['configs'] );
231 // Import config 'strings' and the active tag if a config has it.
232 if ( isset( $config['is_active'] ) ) {
233 $new_data['is_active'] = $config['is_active'];
234 }
235 $new_data['strings'] = $config_component->import_module_strings( $new_data );
236 // Update config data.
237 update_site_option( $k, $new_data );
238 }
239 }
240 }
241 }
242 // For older versions we do not use old models, e.g. for version < 2.2. So the default values will be used.
243 }
244
245 /**
246 * Upgrade to version 2.4.2.
247 *
248 * @return void
249 */
250 private function upgrade_2_4_2(): void {
251 // Update Scan settings.
252 $model_settings = wd_di()->get( Scan_Settings::class );
253 $option = get_site_option( $model_settings->get_table() );
254 if ( ! empty( $option ) && ! is_array( $option ) ) {
255 $old_settings = json_decode( $option, true );
256 if ( is_array( $old_settings ) && isset( $old_settings['max_filesize'] ) ) {
257 $model_settings->filesize = (int) $old_settings['max_filesize'];
258 $model_settings->save();
259 }
260 }
261 // Update 'reminder_duration' value inside the Security Key tweak.
262 $old_settings = get_site_option( 'wd_hardener_settings' );
263 if ( ! empty( $old_settings ) && ! is_array( $old_settings ) ) {
264 $old_settings = json_decode( $old_settings, true );
265 $tweak_sec_key = wd_di()->get( Security_Key::class );
266
267 if ( is_array( $old_settings ) && isset( $old_settings['data']['securityReminderDuration'] )
268 && in_array(
269 $old_settings['data']['securityReminderDuration'],
270 $tweak_sec_key->reminder_frequencies(),
271 true
272 )
273 ) {
274 $tweak_sec_key->update_option( 'reminder_duration', $old_settings['data']['securityReminderDuration'] );
275 }
276 }
277 }
278
279 /**
280 * Migrate value of scan setting from 'integrity_check' to 'check_core'.
281 *
282 * @return void
283 * @since 2.4.7
284 */
285 private function migrate_scan_integrity_check(): void {
286 $model = new Scan_Settings();
287 $model->check_core = (bool) $model->integrity_check;
288 $model->save();
289 }
290
291 /**
292 * Run an upgrade/installation.
293 *
294 * @return void
295 * @throws SodiumException If the encryption fails.
296 */
297 public function run() {
298 // Sometimes multiple requests come at the same time. So we will only count the web requests.
299 if ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) {
300 return;
301 }
302
303 $db_version = get_site_option( 'wd_db_version' );
304 if ( empty( $db_version ) ) {
305 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
306 update_site_option( Feature_Modal::FEATURE_SLUG, true );
307
308 return;
309 }
310
311 if ( DEFENDER_DB_VERSION === $db_version ) {
312 return;
313 }
314 $this->create_database_tables();
315 $this->maybe_show_new_features( $db_version );
316 $this->migrate_configs( $db_version );
317
318 if ( version_compare( $db_version, '2.2.9', '<' ) ) {
319 $this->migrate_security_headers();
320 }
321 if ( version_compare( $db_version, '2.4.2', '<' ) ) {
322 $this->upgrade_2_4_2();
323 }
324 if ( version_compare( $db_version, '2.4.7', '<' ) ) {
325 $this->index_database();
326 $this->migrate_scan_integrity_check();
327 }
328 if ( version_compare( $db_version, '2.4.10', '<' ) ) {
329 $this->upgrade_2_4_10();
330 }
331 if ( version_compare( $db_version, '2.5.0', '<' ) ) {
332 $this->upgrade_2_5_0();
333 }
334 if ( version_compare( $db_version, '2.5.6', '<' ) ) {
335 $this->upgrade_2_5_6();
336 }
337 if ( version_compare( $db_version, '2.6.1', '<' ) ) {
338 $this->upgrade_2_6_1();
339 }
340 if ( version_compare( $db_version, '2.7.0', '<' ) ) {
341 $this->upgrade_2_7_0();
342 }
343 if ( version_compare( $db_version, '2.8.0', '<' ) ) {
344 $this->upgrade_2_8_0();
345 }
346 if ( version_compare( $db_version, '2.8.3', '<' ) ) {
347 $this->upgrade_2_8_3();
348 }
349 if ( version_compare( $db_version, '3.2.0', '<' ) ) {
350 $this->upgrade_3_2_0();
351 }
352 if ( version_compare( $db_version, '3.3.0', '<' ) ) {
353 $this->upgrade_3_3_0();
354 }
355 if ( version_compare( $db_version, '3.3.1', '<' ) ) {
356 $this->upgrade_3_3_1();
357 }
358 if ( version_compare( $db_version, '3.3.3', '<' ) ) {
359 $this->upgrade_3_3_3();
360 }
361 if ( version_compare( $db_version, '3.5.0', '<' ) ) {
362 $this->upgrade_3_5_0();
363 }
364 if ( version_compare( $db_version, '3.8.0', '<' ) ) {
365 $this->upgrade_3_8_0();
366 }
367 if ( version_compare( $db_version, '3.8.2', '<' ) ) {
368 $this->upgrade_3_8_2();
369 }
370 if ( version_compare( $db_version, '3.9.0', '<' ) ) {
371 $this->upgrade_3_9_0();
372 }
373 if ( version_compare( $db_version, '3.11.0', '<' ) ) {
374 $this->upgrade_3_11_0();
375 }
376 if ( version_compare( $db_version, '3.12.0', '<' ) ) {
377 $this->upgrade_3_12_0();
378 }
379 if ( version_compare( $db_version, '4.0.0', '<' ) ) {
380 $this->upgrade_4_0_0();
381 }
382 if ( version_compare( $db_version, '4.1.0', '<' ) ) {
383 $this->upgrade_4_1_0();
384 }
385 if ( version_compare( $db_version, '4.2.0', '<' ) ) {
386 $this->upgrade_4_2_0();
387 }
388 if ( version_compare( $db_version, '4.5.1', '<' ) ) {
389 $this->upgrade_4_5_1();
390 }
391 if ( version_compare( $db_version, '4.6.0', '<' ) ) {
392 $this->upgrade_4_6_0();
393 }
394 if ( version_compare( $db_version, '4.7.2', '<' ) ) {
395 $this->upgrade_4_7_2();
396 }
397 if ( version_compare( $db_version, '4.8.2', '<' ) ) {
398 $this->upgrade_4_8_2();
399 }
400 if ( version_compare( $db_version, '4.9.0', '<' ) ) {
401 $this->upgrade_4_9_0();
402 }
403 if ( version_compare( $db_version, '5.0.0', '<' ) ) {
404 $this->upgrade_5_0_0();
405 }
406 if ( version_compare( $db_version, '5.0.2', '<' ) ) {
407 $this->upgrade_5_0_2();
408 }
409 if ( version_compare( $db_version, '5.1.1', '<' ) ) {
410 $this->upgrade_5_1_1();
411 }
412 if ( version_compare( $db_version, '5.2.0', '<' ) ) {
413 $this->upgrade_5_2_0();
414 }
415 if ( version_compare( $db_version, '5.3.0', '<' ) ) {
416 $this->upgrade_5_3_0();
417 }
418 if ( version_compare( $db_version, '5.3.1', '<' ) ) {
419 $this->upgrade_5_3_1();
420 }
421 if ( version_compare( $db_version, '5.4.0', '<' ) ) {
422 $this->upgrade_5_4_0();
423 }
424 if ( version_compare( $db_version, '5.5.0', '<' ) ) {
425 $this->upgrade_5_5_0();
426 }
427 if ( version_compare( $db_version, '5.6.0', '<' ) ) {
428 $this->upgrade_5_6_0();
429 }
430 if ( version_compare( $db_version, '5.7.0', '<' ) ) {
431 $this->upgrade_5_7_0();
432 }
433 // This is not a new installation. Make a mark.
434 defender_no_fresh_install();
435 // Don't run any function below this line.
436 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
437 }
438
439 /**
440 * Index necessary columns.
441 * Sometimes this function call twice that's why we have to check index already exists or not.
442 * `dbDelta` not work on `ALTER TABLE` query, so we had to use $wpdb->query().
443 *
444 * @return void
445 * @since 2.4.7
446 */
447 private function index_database(): void {
448 global $wpdb;
449 $wpdb->hide_errors();
450
451 $this->add_index_to_defender_email_log( $wpdb );
452 $this->add_index_to_defender_audit_log( $wpdb );
453 $this->add_index_to_defender_scan_item( $wpdb );
454 $this->add_index_to_defender_lockout_log( $wpdb );
455 $this->add_index_to_defender_lockout( $wpdb );
456 }
457
458 /**
459 * Add index to defender_email_log.
460 *
461 * @param wpdb $wpdb WordPress Database object.
462 *
463 * @return void
464 * @since 2.4.7
465 */
466 private function add_index_to_defender_email_log( $wpdb ) {
467 // Check index already exists or not.
468 $result = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->base_prefix}defender_email_log WHERE Key_name = 'source';", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
469 if ( is_array( $result ) ) {
470 return;
471 }
472
473 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_email_log ADD INDEX `source` (`source`);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
474 }
475
476 /**
477 * Add index to defender_audit_log.
478 *
479 * @param wpdb $wpdb WordPress Database object.
480 *
481 * @return void
482 * @since 2.4.7
483 */
484 private function add_index_to_defender_audit_log( $wpdb ) {
485 // Check index already exists or not.
486 $result = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->base_prefix}defender_audit_log WHERE Key_name = 'event_type';", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
487 if ( is_array( $result ) ) {
488 return;
489 }
490
491 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_audit_log ADD INDEX `event_type` (`event_type`), ADD INDEX `action_type` (`action_type`), ADD INDEX `user_id` (`user_id`), ADD INDEX `context` (`context`), ADD INDEX `ip` (`ip`);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
492 }
493
494 /**
495 * Add index to defender_scan_item.
496 *
497 * @param wpdb $wpdb WordPress Database object.
498 *
499 * @return void
500 * @since 2.4.7
501 */
502 private function add_index_to_defender_scan_item( $wpdb ) {
503 // Check index already exists or not.
504 $result = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->base_prefix}defender_scan_item WHERE Key_name = 'type';", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
505 if ( is_array( $result ) ) {
506 return;
507 }
508
509 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_scan_item ADD INDEX `type` (`type`), ADD INDEX `status` (`status`);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
510 }
511
512 /**
513 * Add index to defender_lockout_log.
514 *
515 * @param wpdb $wpdb WordPress Database object.
516 *
517 * @return void
518 * @since 2.4.7
519 */
520 private function add_index_to_defender_lockout_log( $wpdb ) {
521 // Check index already exists or not.
522 $result = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->base_prefix}defender_lockout_log WHERE Key_name = 'ip';", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
523 if ( is_array( $result ) ) {
524 return;
525 }
526
527 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_lockout_log ADD INDEX `ip` (`ip`), ADD INDEX `type` (`type`), ADD INDEX `tried` (`tried`);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
528 }
529
530 /**
531 * Add index to defender_lockout.
532 *
533 * @param wpdb $wpdb WordPress Database object.
534 *
535 * @return void
536 * @since 2.4.7
537 */
538 private function add_index_to_defender_lockout( $wpdb ) {
539 // Check index already exists or not.
540 $result = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->base_prefix}defender_lockout WHERE Key_name = 'ip';", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
541 if ( is_array( $result ) ) {
542 return;
543 }
544
545 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_lockout ADD INDEX `ip` (`ip`), ADD INDEX `status` (`status`), ADD INDEX `attempt` (`attempt`), ADD INDEX `attempt_404` (`attempt_404`);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
546 }
547
548 /**
549 * Upgrade to 2.4.10.
550 *
551 * @return void
552 * @since 2.4.10
553 */
554 private function upgrade_2_4_10(): void {
555 $service = wd_di()->get( Backup_Settings::class );
556 $configs = Config_Hub_Helper::get_configs( $service );
557 $deprecated_keys = array(
558 // Reason: updated or removed some tweak slugs.
559 'security_key',
560 'wp-rest-api',
561 // Reason: moved the security headers to a separate module.
562 'sh-referrer-policy',
563 'sh-strict-transport',
564 'sh-xframe',
565 'sh-content-security',
566 'sh-content-type-options',
567 'sh-feature-policy',
568 'sh-xss-protection',
569 );
570 foreach ( $configs as $key => $config ) {
571 $is_updated = false;
572 // Remove deprecated 'data' key inside Security tweaks.
573 if ( isset( $config['configs']['security_tweaks']['data'] ) ) {
574 unset( $configs[ $key ]['configs']['security_tweaks']['data'] );
575 $is_updated = true;
576 }
577 // Remove deprecated keys in 'issues'.
578 if ( isset( $config['configs']['security_tweaks']['issues'] ) ) {
579 foreach ( $config['configs']['security_tweaks']['issues'] as $iss_key => $issue ) {
580 if ( in_array( $issue, $deprecated_keys, true ) ) {
581 unset( $configs[ $key ]['configs']['security_tweaks']['issues'][ $iss_key ] );
582 $is_updated = true;
583 }
584 }
585 }
586 // In 'ignore'.
587 if ( isset( $config['configs']['security_tweaks']['ignore'] ) ) {
588 foreach ( $config['configs']['security_tweaks']['ignore'] as $ign_key => $issue ) {
589 if ( in_array( $issue, $deprecated_keys, true ) ) {
590 unset( $configs[ $key ]['configs']['security_tweaks']['ignore'][ $ign_key ] );
591 $is_updated = true;
592 }
593 }
594 }
595 // In 'fixed'.
596 if ( isset( $config['configs']['security_tweaks']['fixed'] ) ) {
597 foreach ( $config['configs']['security_tweaks']['fixed'] as $fix_key => $issue ) {
598 if ( in_array( $issue, $deprecated_keys, true ) ) {
599 unset( $configs[ $key ]['configs']['security_tweaks']['fixed'][ $fix_key ] );
600 $is_updated = true;
601 }
602 }
603 }
604
605 if ( $is_updated ) {
606 update_site_option( $key, $configs[ $key ] );
607 Config_Hub_Helper::update_on_hub( $configs[ $key ] );
608 if ( isset( $config['is_active'] ) ) {
609 $model = new Model\Setting\Security_Tweaks();
610 $model->issues = $configs[ $key ]['configs']['security_tweaks']['issues'];
611 $model->ignore = $configs[ $key ]['configs']['security_tweaks']['ignore'];
612 $model->fixed = $configs[ $key ]['configs']['security_tweaks']['fixed'];
613 $model->save();
614 }
615 }
616 }
617 }
618
619 /**
620 * Upgrade to 2.5.0.
621 *
622 * @return void
623 * @since 2.5.0
624 */
625 private function upgrade_2_5_0(): void {
626 $model = wd_di()->get( Security_Headers::class );
627 // Directive ALLOW-FROM is deprecated. If header directive is ALLOW-FROM then set 'sameorigin'.
628 if ( isset( $model->sh_xframe_mode ) && 'allow-from' === $model->sh_xframe_mode ) {
629 $model->sh_xframe_mode = 'sameorigin';
630 $model->save();
631 }
632 /**
633 * Uncheck 'File change detection' option if there was checked only child 'Scan theme files' option and save
634 * settings. Also remove items for 'Scan theme files' without run Scan.
635 */
636 $scan_settings = new Scan_Settings();
637 if (
638 $scan_settings->integrity_check
639 && ! $scan_settings->check_core
640 && ! $scan_settings->check_plugins
641 ) {
642 $scan_settings->integrity_check = false;
643 $scan_settings->save();
644 }
645 }
646
647 /**
648 * Forces the addition of default lockout exclusions to the Notfound Lockout settings.
649 *
650 * @return void
651 */
652 private function force_nf_lockout_exclusions(): void {
653 $nf_settings = new Notfound_Lockout();
654 $allowlist = $nf_settings->get_lockout_list( 'allowlist' );
655 $default_allowlist = array( '/cdn-cgi/challenge-platform/' );
656 $is_save = false;
657 if ( ! empty( $allowlist ) ) {
658 foreach ( $default_allowlist as $item ) {
659 if ( ! in_array( $item, $allowlist, true ) ) {
660 $allowlist[] = $item;
661 $is_save = true;
662 }
663 }
664 $nf_settings->whitelist = implode( "\n", $allowlist );
665 } else {
666 $nf_settings->whitelist = ".css\n.js\n.map\n/cdn-cgi/challenge-platform/";
667 $is_save = true;
668 }
669 // Save it.
670 if ( $is_save ) {
671 $nf_settings->save();
672 }
673 }
674
675 /**
676 * Upgrade to 2.5.6.
677 *
678 * @return void
679 * @since 2.5.6
680 */
681 private function upgrade_2_5_6(): void {
682 $adapted_component = wd_di()->get( Legacy_Versions::class );
683 $issue_list = $adapted_component->get_scan_issue_data();
684 $ignored_list = $adapted_component->get_scan_ignored_data();
685 if ( ! empty( $issue_list ) || ! empty( $ignored_list ) ) {
686 $adapted_component->migrate_scan_data( $issue_list, $ignored_list );
687 $adapted_component->remove_old_scan_data( $issue_list, $ignored_list );
688 $adapted_component->change_onboarding_status();
689 }
690
691 // Trigger recurring cron schedule for autogenerate security salt.
692 $security_tweaks = wd_di()->get( Security_Tweaks::class );
693 $security_tweaks->get_security_key()->cron_schedule();
694 // Add some lockout extension to old installations forced.
695 $this->force_nf_lockout_exclusions();
696 }
697
698 /**
699 * Updates the body of the scan error email template.
700 *
701 * @param mixed $model The model containing the email template configuration.
702 *
703 * @return void
704 */
705 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 ) {
710 $needle = '{follow this link} and check the logs to see what casued the failure';
711 if ( false !== stripos( $model->configs['template']['error']['body'], $needle ) ) {
712 $model->configs['template']['error']['body'] = str_replace(
713 $needle,
714 'visit your site and run a manual scan',
715 $model->configs['template']['error']['body']
716 );
717 $model->save();
718 }
719 }
720 }
721
722 /**
723 * Upgrade to 2.6.1.
724 *
725 * @return void
726 * @since 2.6.1
727 */
728 private function upgrade_2_6_1(): void {
729 // Update the title of the basic config.
730 $config_component = wd_di()->get( Backup_Settings::class );
731 $configs = $config_component->get_configs();
732 if ( ! empty( $configs ) ) {
733 foreach ( $configs as $k => $config ) {
734 if ( 0 === strcmp( $config['name'], esc_html__( 'Basic config', 'defender-security' ) ) ) {
735 $config['name'] = esc_html__( 'Basic Config', 'defender-security' );
736
737 update_site_option( $k, $config );
738 delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
739 break;
740 }
741 }
742 }
743 // Update scan emails for 'When failed to scan' option.
744 $malware_notification = wd_di()->get( Malware_Notification::class );
745 $this->update_scan_error_send_body( $malware_notification );
746 $malware_report = wd_di()->get( Malware_Report::class );
747 $this->update_scan_error_send_body( $malware_report );
748 }
749
750 /**
751 * Update 2FA email template.
752 *
753 * @return void
754 */
755 private function update_2fa_send_body(): void {
756 $model = wd_di()->get( Two_Fa_Settings::class );
757 if ( isset( $model->email_body ) ) {
758 $model->email_body = 'Hi {{display_name}},
759
760 Your temporary password is {{passcode}}. To finish logging in, copy and paste the temporary password into the Password field on the login screen.';
761 $model->save();
762 }
763 }
764
765 /**
766 * Update Malware Scan email template.
767 *
768 * @return void
769 * @since 2.7.0
770 */
771 private function update_malware_scan_send_body(): void {
772 $models = array(
773 wd_di()->get( Malware_Notification::class ),
774 wd_di()->get( Malware_Report::class ),
775 );
776
777 foreach ( $models as $model ) {
778 $is_updated = false;
779 if ( ! empty( $model->configs['template']['error']['body'] ) ) {
780 $subject = $this->replace_scan_email_content_error( $model->configs['template']['error']['body'] );
781
782 if ( $model->configs['template']['error']['body'] !== $subject ) {
783 $is_updated = true;
784
785 $model->configs['template']['error']['body'] = $subject;
786 }
787 }
788
789 if ( ! empty( $model->configs['template']['found']['body'] ) ) {
790 $subject = $this->replace_scan_email_content_issue_found( $model->configs['template']['found']['body'] );
791
792 if ( $model->configs['template']['found']['body'] !== $subject ) {
793 $is_updated = true;
794
795 $model->configs['template']['found']['body'] = $subject;
796 }
797 }
798
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'] );
801
802 if ( $model->configs['template']['not_found']['body'] !== $subject ) {
803 $is_updated = true;
804
805 $model->configs['template']['not_found']['body'] = $subject;
806 }
807 }
808
809 if ( true === $is_updated ) {
810 $model->save();
811 }
812 }
813 }
814
815 /**
816 * Update Malware Scan email template common parts.
817 *
818 * @param string $subject Email subject.
819 *
820 * @return string
821 * @since 2.7.0
822 */
823 private function update_malware_scan_send_body_common( $subject ): string {
824 $subject = preg_replace( '/(\R+)WP Defender here, reporting back from the front\./i', '', $subject );
825
826 return preg_replace( '/(\R+)Stay Safe,(\R+)WP Defender(\R+)WPMU DEV Superhero(\s*)$/i', '', $subject );
827 }
828
829 /**
830 * Upgrade to 2.7.0.
831 *
832 * @return void
833 * @since 2.7.0
834 */
835 private function upgrade_2_7_0(): void {
836 $malware_report = wd_di()->get( Malware_Report::class );
837 $scan_settings = wd_di()->get( Scan_Settings::class );
838 // Migrate data from Malware_Report to Scan settings.
839 $scan_settings->scheduled_scanning = Notification::STATUS_ACTIVE === $malware_report->status;
840 $scan_settings->frequency = $malware_report->frequency;
841 $scan_settings->day = $malware_report->day;
842 $scan_settings->day_n = (int) $malware_report->day_n;
843 $scan_settings->time = $malware_report->time;
844 $scan_settings->save();
845
846 $this->update_2fa_send_body();
847 $this->update_malware_scan_send_body();
848 }
849
850 /**
851 * Update in_house recipients empty role.
852 *
853 * @return void
854 * @since 2.8.0
855 */
856 private function update_in_house_recipients_empty_role(): void {
857 $models = array(
858 wd_di()->get( Audit_Report::class ),
859 wd_di()->get( Firewall_Notification::class ),
860 wd_di()->get( Firewall_Report::class ),
861 wd_di()->get( Malware_Notification::class ),
862 wd_di()->get( Malware_Report::class ),
863 wd_di()->get( Tweak_Reminder::class ),
864 );
865
866 foreach ( $models as $model ) {
867 if ( ! empty( $model->in_house_recipients ) && is_array( $model->in_house_recipients ) ) {
868 $is_updated = false;
869
870 foreach ( $model->in_house_recipients as &$recipient ) {
871 if ( empty( $recipient['role'] ) ) {
872 $is_updated = true;
873 $recipient['role'] = $this->get_current_user_role( $recipient['id'] );
874 }
875 }
876
877 if ( true === $is_updated ) {
878 $model->save();
879 }
880 }
881 }
882 }
883
884 /**
885 * Delete temp geolite file from /wp-admin directory.
886 *
887 * @return void
888 * @since 2.8.0
889 */
890 private function delete_tmp_geolite_file(): void {
891 $pattern = ABSPATH . 'wp-admin/geolite2-country*.tar.gz';
892 array_map( 'wp_delete_file', glob( $pattern ) );
893 }
894
895 /**
896 * Migrate old keys to checked options.
897 *
898 * @return void
899 * @since 2.8.0
900 */
901 private function update_2fa_methods(): void {
902 $settings = wd_di()->get( Two_Fa_Settings::class );
903 if ( $settings->enabled ) {
904 $service = wd_di()->get( Two_Fa_Component::class );
905 $query = new WP_User_Query(
906 array(
907 'blog_id' => 0,
908 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
909 array(
910 'key' => Totp::TOTP_AUTH_KEY,
911 'value' => true,
912 ),
913 ),
914 'fields' => 'ID',
915 )
916 );
917 if ( $query->get_total() > 0 ) {
918 // The data is independent of the user's data.
919 $providers = $service->get_providers();
920 $totp_slug = Totp::$slug;
921 // Add TOTP slug.
922 $enabled_providers = array( $totp_slug );
923 // If 'Enable lost phone' option is checked then we add the slug of the Email provider too.
924 if ( true === $settings->lost_phone ) {
925 $enabled_providers[] = Fallback_Email::$slug;
926 }
927 // Add slugs to the list of the enabled providers.
928 $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
929 foreach ( $query->get_results() as $user_id ) {
930 // If 2FA module is configured for the current user.
931 if ( $service->is_enabled_otp_for_user( $user_id ) ) {
932 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
933 // Set TOTP as the default auth provider.
934 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $totp_slug );
935 }
936 }
937 }
938 }
939 }
940
941 /**
942 * Upgrade to 2.8.0.
943 *
944 * @return void
945 * @since 2.8.0
946 */
947 private function upgrade_2_8_0(): void {
948 $this->update_in_house_recipients_empty_role();
949 $this->delete_tmp_geolite_file();
950 $this->update_2fa_methods();
951 }
952
953 /**
954 * Add column country_iso_code and index to table.
955 *
956 * @return void
957 * @since 2.8.1
958 */
959 private function add_country_iso_code_column(): void {
960 if ( ! function_exists( 'maybe_add_column' ) ) {
961 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
962 }
963
964 global $wpdb;
965
966 $table_name = $wpdb->base_prefix . 'defender_lockout_log';
967 $column_name = 'country_iso_code';
968 $create_ddl = "ALTER TABLE {$table_name} ADD {$column_name} CHAR(2) DEFAULT NULL";
969
970 maybe_add_column( $table_name, $column_name, $create_ddl );
971
972 $prev_val = $wpdb->hide_errors();
973 $wpdb->query( "CREATE INDEX `country_iso_code` ON {$wpdb->base_prefix}defender_lockout_log (`country_iso_code`)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
974 $wpdb->show_errors( $prev_val );
975 }
976
977 /**
978 * Upgrade to 2.8.3.
979 *
980 * @return void
981 * @since 2.8.3
982 */
983 private function upgrade_2_8_3(): void {
984 $this->add_country_iso_code_column();
985 }
986
987 /**
988 * Upgrade to 3.2.0.
989 *
990 * @return void
991 * @since 3.2.0
992 */
993 private function upgrade_3_2_0(): void {
994 $model = wd_di()->get( Two_Fa_Settings::class );
995
996 if ( ! empty( $model->app_title ) ) {
997 $model->app_title = wp_specialchars_decode( $model->app_title, ENT_QUOTES );
998 }
999
1000 $model->custom_graphic_type = Two_Fa_Settings::CUSTOM_GRAPHIC_TYPE_UPLOAD;
1001 $model->save();
1002 }
1003
1004 /**
1005 * Upgrade to 3.3.0.
1006 *
1007 * @return void
1008 * @since 3.3.0
1009 */
1010 private function upgrade_3_3_0(): void {
1011 $this->update_webauthn_user_handle();
1012 $this->add_ua_lockout_to_firewall_notification();
1013 }
1014
1015 /**
1016 * Update webauthn 'userHandle' to make it independent of salt keys.
1017 *
1018 * @return void
1019 * @since 3.3.0
1020 */
1021 private function update_webauthn_user_handle(): void {
1022 global $wpdb;
1023 $service = wd_di()->get( Webauthn::class );
1024
1025 if ( is_multisite() ) {
1026 $offset = 0;
1027 $limit = 100;
1028 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1029 $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
1030 ARRAY_A
1031 );
1032 while ( ! empty( $blogs ) && is_array( $blogs ) ) {
1033 foreach ( $blogs as $blog ) {
1034 switch_to_blog( $blog['blog_id'] );
1035
1036 $this->update_webauthn_user_handle_core( $service );
1037
1038 restore_current_blog();
1039 }
1040 $offset += $limit;
1041 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1042 $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
1043 ARRAY_A
1044 );
1045 }
1046 } else {
1047 $this->update_webauthn_user_handle_core( $service );
1048 }
1049 }
1050
1051 /**
1052 * Core method for updating webauthn 'userHandle' to make it independent of salt keys.
1053 *
1054 * @param object $service Webauthn service instance.
1055 *
1056 * @return void
1057 * @since 3.3.0
1058 */
1059 private function update_webauthn_user_handle_core( object $service ) {
1060 $data = get_option( $this->option_prefix . $service::CREDENTIAL_OPTION_KEY );
1061 $data = is_string( $data ) ? json_decode( $data, true ) : array();
1062 delete_option( $this->option_prefix . $service::CREDENTIAL_OPTION_KEY );
1063 if ( ! is_array( $data ) || 0 === count( $data ) ) {
1064 return;
1065 }
1066
1067 $users = get_users( array( 'fields' => array( 'ID', 'user_login', 'display_name' ) ) );
1068
1069 foreach ( $users as $user ) {
1070 if ( empty( $data ) ) {
1071 break;
1072 }
1073
1074 $user_credentials = array();
1075 foreach ( $data as $key => $item ) {
1076 $old_hash = hash( 'sha256', $user->user_login . '-' . $user->display_name . '-' . AUTH_SALT );
1077 // This is not obfuscation. Just encode the hashed value into a base64 string.
1078 $old_base64_hash = base64_encode( $old_hash ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1079 $old_base64_hash = preg_replace( '/\=+$/', '', $old_base64_hash );
1080
1081 if (
1082 isset( $item['credential_source']['userHandle'] ) &&
1083 $item['credential_source']['userHandle'] === $old_base64_hash
1084 ) {
1085 $new_hash = $this->get_user_hash( $user->user_login );
1086 // This is not obfuscation. Just encode the hashed value into a base64 string.
1087 $new_base64_hash = base64_encode( $new_hash ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1088 $new_base64_hash = preg_replace( '/\=+$/', '', $new_base64_hash );
1089 $item['user'] = $new_hash;
1090 $item['credential_source']['userHandle'] = $new_base64_hash;
1091 $user_credentials[ $key ] = $item;
1092 unset( $data[ $key ] );
1093 }
1094 }
1095
1096 if ( ! empty( $user_credentials ) ) {
1097 $service->setCredentials( (int) $user->ID, $user_credentials );
1098 }
1099 }
1100 }
1101
1102 /**
1103 * Add the ability to send notifications about UA-lockout.
1104 *
1105 * @return void
1106 */
1107 private function add_ua_lockout_to_firewall_notification(): void {
1108 $model = wd_di()->get( Firewall_Notification::class );
1109 if ( ! isset( $model->configs['ua_lockout'] ) ) {
1110 $model->configs['ua_lockout'] = false;
1111 $model->save();
1112 }
1113 }
1114
1115 /**
1116 * Upgrade to 3.3.1: extra security steps.
1117 *
1118 * @return void
1119 */
1120 private function upgrade_3_3_1(): void {
1121 // Update tokens.
1122 $query = new WP_User_Query(
1123 array(
1124 'blog_id' => 0,
1125 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1126 array(
1127 'key' => Two_Fa_Component::TOKEN_USER_KEY,
1128 'compare' => 'EXISTS',
1129 ),
1130 ),
1131 'fields' => 'ID',
1132 )
1133 );
1134 if ( $query->get_total() > 0 ) {
1135 // Hashed tokens.
1136 foreach ( $query->get_results() as $user_id ) {
1137 $token = bin2hex( Crypt::random_bytes( 32 ) );
1138 update_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, wp_hash( $user_id . $token ) );
1139 }
1140 }
1141 // Update fallback codes.
1142 $query = new WP_User_Query(
1143 array(
1144 'blog_id' => 0,
1145 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1146 array(
1147 'key' => Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
1148 'compare' => 'EXISTS',
1149 ),
1150 ),
1151 'fields' => 'ID',
1152 )
1153 );
1154 if ( $query->get_total() > 0 ) {
1155 // Hashed codes.
1156 foreach ( $query->get_results() as $user_id ) {
1157 $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'] ) ) {
1159 update_user_meta(
1160 $user_id,
1161 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
1162 array(
1163 'code' => wp_hash( $backup_code['code'] ),
1164 'time' => $backup_code['time'],
1165 )
1166 );
1167 }
1168 }
1169 }
1170 }
1171
1172 /**
1173 * Upgrade to 3.3.3: update 2FA flow for secret keys.
1174 *
1175 * @return void
1176 * @throws SodiumException If the encryption fails.
1177 * @since 3.3.3
1178 */
1179 private function upgrade_3_3_3(): void {
1180 // Create a file with a random key if it doesn't exist.
1181 if ( ( new Crypt() )->create_key_file() ) {
1182 // Get user data.
1183 $query = new WP_User_Query(
1184 array(
1185 'blog_id' => 0,
1186 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1187 array(
1188 'key' => Totp::TOTP_SODIUM_SECRET_KEY,
1189 'compare' => 'EXISTS',
1190 ),
1191 ),
1192 'fields' => 'ID',
1193 )
1194 );
1195 if ( $query->get_total() > 0 ) {
1196 $service = wd_di()->get( Two_Fa_Component::class );
1197 foreach ( $query->get_results() as $user_id ) {
1198 // If we have the old states (cleartext, pub key) then re-encrypt via Sodium and save it.
1199 if ( ! $service->maybe_update( $user_id ) ) {
1200 // Otherwise just encrypt it via Sodium.
1201 $plaintext = defender_generate_random_string( Totp::TOTP_LENGTH, TOTP::TOTP_CHARACTERS );
1202 $new_key = Crypt::get_encrypted_data( $plaintext );
1203 if ( ! is_wp_error( $new_key ) ) {
1204 // Remove an old key.
1205 delete_user_meta( $user_id, Totp::TOTP_SECRET_KEY );
1206 // Update a new one.
1207 update_user_meta( $user_id, Totp::TOTP_SODIUM_SECRET_KEY, $new_key );
1208 }
1209 }
1210 }
1211 }
1212 }
1213 }
1214
1215 /**
1216 * Upgrade to 3.5.0: Delete config transient.
1217 *
1218 * @return void
1219 */
1220 private function upgrade_3_5_0(): void {
1221 delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
1222 }
1223
1224 /**
1225 * Upgrade to 3.8.0: Remove permanently banned ip record from `defender_lockout` table.
1226 *
1227 * @return void
1228 * @since 3.8.0
1229 */
1230 private function upgrade_3_8_0(): void {
1231 global $wpdb;
1232
1233 $blacklist_settings = new Model_Blacklist_Lockout();
1234 $blacklist = $blacklist_settings->get_list();
1235
1236 $offset = 0;
1237 $length = 100;
1238 $table = $wpdb->base_prefix . 'defender_lockout';
1239 // Variable within condition is for comparison.
1240 while ( $blacklist_chunk = array_slice( $blacklist, $offset, $length ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
1241 $sql = "DELETE FROM {$table} WHERE status = 'blocked' AND ip IN (" . implode( ', ', array_fill( 0, count( $blacklist_chunk ), '%s' ) ) . ')';
1242
1243 $query = call_user_func_array(
1244 array( $wpdb, 'prepare' ),
1245 array_merge( array( $sql ), $blacklist_chunk )
1246 );
1247 // SQL is prepared here. So we don't need to prepare it again.
1248 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
1249
1250 $offset += $length;
1251 }
1252
1253 $this->update_malware_notification_config();
1254 $this->update_malware_notification_setting();
1255 }
1256
1257 /**
1258 * Update malware notification config.
1259 *
1260 * @return void
1261 * @since 3.8.0
1262 */
1263 private function update_malware_notification_config(): void {
1264 $service = wd_di()->get( Backup_Settings::class );
1265 $configs = Config_Hub_Helper::get_configs( $service );
1266
1267 foreach ( $configs as $key => $config ) {
1268 $is_updated = false;
1269
1270 if ( isset( $config['configs']['scan']['email_subject_issue_found'] ) ) {
1271 $subject = $this->replace_scan_email_subject_issue_found( $config['configs']['scan']['email_subject_issue_found'] );
1272 $configs[ $key ]['configs']['scan']['email_subject_issue_found'] = $subject;
1273 $is_updated = true;
1274 }
1275 if ( isset( $config['configs']['scan']['email_content_issue_found'] ) ) {
1276 $content = $this->replace_scan_email_content_issue_found( $config['configs']['scan']['email_content_issue_found'] );
1277 $content = $this->replace_scan_email_content_issue_found_new( $content );
1278 $configs[ $key ]['configs']['scan']['email_content_issue_found'] = $content;
1279 $is_updated = true;
1280 }
1281
1282 if ( isset( $config['configs']['scan']['email_content_issue_not_found'] ) ) {
1283 $content = $this->replace_scan_email_content_issue_not_found( $config['configs']['scan']['email_content_issue_not_found'] );
1284 $configs[ $key ]['configs']['scan']['email_content_issue_not_found'] = $content;
1285 $is_updated = true;
1286 }
1287
1288 if ( isset( $config['configs']['scan']['email_content_error'] ) ) {
1289 $content = $this->replace_scan_email_content_error( $config['configs']['scan']['email_content_error'] );
1290 $configs[ $key ]['configs']['scan']['email_content_error'] = $content;
1291 $is_updated = true;
1292 }
1293
1294 if ( true === $is_updated ) {
1295 update_site_option( $key, $configs[ $key ] );
1296 Config_Hub_Helper::update_on_hub( $configs[ $key ] );
1297 }
1298 }
1299
1300 delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
1301 }
1302
1303 /**
1304 * Update malware notification setting.
1305 *
1306 * @return void
1307 * @since 3.8.0
1308 */
1309 private function update_malware_notification_setting(): void {
1310 $models = array(
1311 wd_di()->get( Malware_Notification::class ),
1312 wd_di()->get( Malware_Report::class ),
1313 );
1314
1315 foreach ( $models as $model ) {
1316 $is_updated = false;
1317
1318 if ( isset( $model->configs['template']['found']['subject'] ) ) {
1319 $subject = $this->replace_scan_email_subject_issue_found( $model->configs['template']['found']['subject'] );
1320 $model->configs['template']['found']['subject'] = $subject;
1321 $is_updated = true;
1322 }
1323 if ( isset( $model->configs['template']['found']['body'] ) ) {
1324 $content = $this->replace_scan_email_content_issue_found( $model->configs['template']['found']['body'] );
1325 $content = $this->replace_scan_email_content_issue_found_new( $content );
1326 $model->configs['template']['found']['body'] = $content;
1327 $is_updated = true;
1328 }
1329
1330 if ( isset( $model->configs['template']['not_found']['body'] ) ) {
1331 $content = $this->replace_scan_email_content_issue_not_found( $model->configs['template']['not_found']['body'] );
1332 $model->configs['template']['not_found']['body'] = $content;
1333 $is_updated = true;
1334 }
1335
1336 if ( isset( $model->configs['template']['error']['body'] ) ) {
1337 $content = $this->replace_scan_email_content_error( $model->configs['template']['error']['body'] );
1338 $model->configs['template']['error']['body'] = $content;
1339 $is_updated = true;
1340 }
1341
1342 if ( true === $is_updated ) {
1343 $model->save();
1344 }
1345 }
1346 }
1347
1348 /**
1349 * Replace scan issue found email subject.
1350 *
1351 * @param string $subject Email subject.
1352 *
1353 * @return string
1354 * @since 3.8.0
1355 */
1356 private function replace_scan_email_subject_issue_found( string $subject ): string {
1357 $subject = str_replace(
1358 esc_html__( 'Scan of {SITE_URL} complete.', 'defender-security' ),
1359 esc_html__( 'Malware Scan of {SITE_URL} is complete.', 'defender-security' ),
1360 $subject
1361 );
1362
1363 return str_replace(
1364 esc_html__( '{ISSUES_COUNT} issues found.', 'defender-security' ),
1365 esc_html__( '{ISSUES_COUNT} issue(s) found.', 'defender-security' ),
1366 $subject
1367 );
1368 }
1369
1370 /**
1371 * Replace scan issue found email content.
1372 *
1373 * @param string $content Email content.
1374 *
1375 * @return string
1376 * @since 3.8.0
1377 */
1378 private function replace_scan_email_content_issue_found( string $content ): string {
1379 $content = $this->update_malware_scan_send_body_common( $content );
1380 $content = preg_replace(
1381 "/I(’|'|\\\')ve finished scanning \{SITE_URL\} for vulnerabilities and I found \{ISSUES_COUNT\} issues that you should take a closer look at!/i",
1382 '{ISSUES_COUNT} vulnerabilities were identified for {SITE_URL} during a Malware Scan. See details for each issue below.',
1383 $content
1384 );
1385
1386 return preg_replace( '/(\R+){ISSUES_LIST}/i', "\n\n{ISSUES_LIST}", $content );
1387 }
1388
1389 /**
1390 * Replace scan issue found email with new content.
1391 *
1392 * @param string $content Email content.
1393 *
1394 * @return string
1395 * @since 3.8.0
1396 */
1397 private function replace_scan_email_content_issue_found_new( string $content ): string {
1398 $content = str_replace(
1399 esc_html__( '{ISSUES_COUNT} vulnerabilities were identified for {SITE_URL} during a Malware Scan.', 'defender-security' ),
1400 esc_html__( 'Malware Scan identified {ISSUES_COUNT} issue(s) on {SITE_URL}.', 'defender-security' ),
1401 $content
1402 );
1403
1404 return str_replace(
1405 esc_html__( 'See details for each issue below.', 'defender-security' ),
1406 esc_html__( 'The identified issue(s) is/are listed below.', 'defender-security' ),
1407 $content
1408 );
1409 }
1410
1411 /**
1412 * Replace scan issue not found email content.
1413 *
1414 * @param string $content Email content.
1415 *
1416 * @return string
1417 * @since 3.8.0
1418 */
1419 private function replace_scan_email_content_issue_not_found( string $content ): string {
1420 $content = $this->update_malware_scan_send_body_common( $content );
1421 $content = preg_replace(
1422 "/I(’|'|\\\')ve finished scanning \{SITE_URL\} for vulnerabilities and I found nothing\. Well done for running such a tight ship!/i",
1423 'No vulnerabilities have been found for {SITE_URL}.',
1424 $content
1425 );
1426
1427 return preg_replace(
1428 "/(\R+)Keep up the good work! With regular security scans and a well-hardened installation you\'ll be just fine\./i",
1429 '',
1430 $content
1431 );
1432 }
1433
1434 /**
1435 * Replace scan error email content.
1436 *
1437 * @param string $content Email content.
1438 *
1439 * @return string
1440 * @since 3.8.0
1441 */
1442 private function replace_scan_email_content_error( string $content ): string {
1443 $content = $this->update_malware_scan_send_body_common( $content );
1444
1445 return str_replace( 'I couldn', 'We couldn', $content );
1446 }
1447
1448 /**
1449 * Upgrade to 3.8.2: Check XSS in existing configs.
1450 *
1451 * @return void
1452 */
1453 private function upgrade_3_8_2(): void {
1454 $service = wd_di()->get( Backup_Settings::class );
1455 $configs = Config_Hub_Helper::get_configs( $service );
1456
1457 foreach ( $configs as $key => $config ) {
1458 $is_updated = false;
1459
1460 if ( isset( $config['name'] ) ) {
1461 $prev_name = $config['name'];
1462 // Sanitize data.
1463 $new_name = sanitize_text_field( $config['name'] );
1464 if ( $prev_name !== $new_name ) {
1465 $is_updated = true;
1466 }
1467 $prev_desc = $config['description'];
1468 $new_desc = empty( $config['description'] ) ? '' : sanitize_textarea_field( $config['description'] );
1469 if ( $prev_desc !== $new_desc ) {
1470 $is_updated = true;
1471 }
1472 }
1473
1474 if ( true === $is_updated ) {
1475 update_site_option( $key, $configs[ $key ] );
1476 Config_Hub_Helper::update_on_hub( $configs[ $key ] );
1477 }
1478 }
1479
1480 delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
1481 }
1482
1483 /**
1484 * Upgrade to 3.9.0.
1485 *
1486 * @return void
1487 */
1488 private function upgrade_3_9_0(): void {
1489 global $wpdb;
1490
1491 $prev_val = $wpdb->hide_errors();
1492 // Changes for Lockout table.
1493 $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_lockout MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1494 // Changes for Lockout log table.
1495 $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 $wpdb->show_errors( $prev_val );
1499 }
1500
1501 /**
1502 * Upgrade to 3.11.0.
1503 *
1504 * @return void
1505 */
1506 private function upgrade_3_11_0(): void {
1507 // Move Global IP settings.
1508 $option = get_site_option( wd_di()->get( Model_Blacklist_Lockout::class )->get_table() );
1509 if ( ! empty( $option ) && is_string( $option ) ) {
1510 $old_settings = json_decode( $option, true );
1511 if ( is_array( $old_settings ) && isset( $old_settings['global_ip_list'] ) ) {
1512 $model_global_ip = wd_di()->get( Global_Ip_Lockout::class );
1513 $model_global_ip->enabled = (bool) $old_settings['global_ip_list'];
1514 $model_global_ip->save();
1515 }
1516 }
1517 }
1518
1519 /**
1520 * Upgrade to 3.12.0.
1521 *
1522 * @return void
1523 */
1524 private function upgrade_3_12_0(): void {
1525 delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
1526
1527 $service = wd_di()->get( Backup_Settings::class );
1528 $configs = Config_Hub_Helper::get_configs( $service );
1529
1530 foreach ( $configs as $key => &$config ) {
1531 if ( ! is_array( $config ) ) {
1532 continue;
1533 }
1534
1535 $config['is_removable'] = $config['is_removable'] ?? true;
1536
1537 // Update config data.
1538 update_site_option( $key, $config );
1539 }
1540
1541 set_site_transient(
1542 Config_Hub_Helper::CONFIGS_TRANSIENT_KEY,
1543 $configs,
1544 Config_Hub_Helper::CONFIGS_TRANSIENT_TIME
1545 );
1546 update_site_option( Config_Hub_Helper::CONFIGS_TRANSIENT_TIME_KEY, time() );
1547
1548 // Clear Global IP schedule hook to update event frequency.
1549 wp_clear_scheduled_hook( 'wpdef_fetch_global_ip_list' );
1550 }
1551
1552 /**
1553 * Upgrade to 4.0.0: Creates quarantine table.
1554 *
1555 * @return void
1556 */
1557 private function upgrade_4_0_0(): void {
1558 $bootstrap = wd_di()->get( Bootstrap::class );
1559 $bootstrap->create_table_quarantine();
1560 }
1561
1562 /**
1563 * Upgrade to 4.1.0.
1564 *
1565 * @return void
1566 */
1567 private function upgrade_4_1_0() {
1568 // Remove Maxmind DB directory from multisite subsites.
1569 if ( is_multisite() ) {
1570 global $wpdb, $wp_filesystem;
1571
1572 if ( is_null( $wp_filesystem ) ) {
1573 if ( ! function_exists( 'WP_Filesystem' ) ) {
1574 require_once ABSPATH . 'wp-admin/includes/file.php';
1575 }
1576 WP_Filesystem();
1577 }
1578 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
1579 return;
1580 }
1581
1582 $main_site_id = get_main_site_id();
1583 $offset = 0;
1584 $limit = 100;
1585 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1586 $wpdb->prepare(
1587 "SELECT blog_id FROM {$wpdb->blogs} WHERE blog_id != %d LIMIT %d, %d",
1588 $main_site_id,
1589 $offset,
1590 $limit
1591 ),
1592 ARRAY_A
1593 );
1594 while ( ! empty( $blogs ) && is_array( $blogs ) ) {
1595 foreach ( $blogs as $blog ) {
1596 switch_to_blog( $blog['blog_id'] );
1597
1598 $maxmind_dir = $this->get_tmp_path() . DIRECTORY_SEPARATOR . MaxMind_Geolocation::DB_DIRECTORY;
1599 $wp_filesystem->delete( $maxmind_dir, true );
1600
1601 restore_current_blog();
1602 }
1603 $offset += $limit;
1604 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1605 $wpdb->prepare(
1606 "SELECT blog_id FROM {$wpdb->blogs} WHERE blog_id != %d LIMIT %d, %d",
1607 $main_site_id,
1608 $offset,
1609 $limit
1610 ),
1611 ARRAY_A
1612 );
1613 }
1614 }
1615 }
1616
1617 /**
1618 * Upgrade to 4.2.0.
1619 *
1620 * @return void
1621 */
1622 private function upgrade_4_2_0(): void {
1623 // Add the Tracking modal.
1624 update_site_option( Data_Tracking::TRACKING_SLUG, true );
1625 }
1626
1627 /**
1628 * Upgrade to 4.5.1.
1629 *
1630 * @return void
1631 */
1632 private function upgrade_4_5_1(): void {
1633 $service = wd_di()->get( Firewall::class );
1634 $service->auto_switch_ip_detection_option();
1635 $service->maybe_show_misconfigured_ip_detection_option_notice();
1636 }
1637
1638 /**
1639 * Upgrade to 4.6.0.
1640 *
1641 * @return void
1642 */
1643 private function upgrade_4_6_0(): void {
1644 // Create Unlockout table.
1645 $bootstrap = wd_di()->get( Bootstrap::class );
1646 $bootstrap->create_table_unlockout();
1647 }
1648
1649 /**
1650 * Upgrade to 4.7.2.
1651 *
1652 * @return void
1653 */
1654 private function upgrade_4_7_2(): void {
1655 $model = wd_di()->get( Two_Fa_Settings::class );
1656
1657 if ( isset( $model->email_body ) ) {
1658 $model->email_body = 'Hi {{display_name}},
1659
1660 Your temporary password is {{passcode}}
1661 To complete your login, copy and paste the temporary password into the Password field on the login screen.';
1662 $model->save();
1663 }
1664 }
1665
1666 /**
1667 * Upgrade to 4.8.2.
1668 *
1669 * @return void
1670 */
1671 private function upgrade_4_8_2(): void {
1672 $xff = defender_get_data_from_request( 'HTTP_X_FORWARDED_FOR', 's' );
1673 if (
1674 ! ( is_string( $xff ) && 0 < strlen( $xff ) ) &&
1675 Firewall::is_switched_ip_detection_notice( Firewall::IP_DETECTION_XFF_SHOW_SLUG )
1676 ) {
1677 delete_site_option( Firewall::IP_DETECTION_XFF_SHOW_SLUG );
1678 }
1679 }
1680
1681 /**
1682 * Upgrade to 4.9.0.
1683 *
1684 * @return void
1685 */
1686 private function upgrade_4_9_0(): void {
1687 $model = wd_di()->get( Model_Firewall::class );
1688
1689 $model->ip_detection_type = 'manual';
1690 $model->save();
1691
1692 // Add tracking.
1693 $firewall_analytics = wd_di()->get( Firewall_Analytics::class );
1694 $detection_method = Firewall_Analytics::get_detection_method_label(
1695 $model->ip_detection_type,
1696 $model->http_ip_header
1697 );
1698
1699 $firewall_analytics->track_feature(
1700 Firewall_Analytics::EVENT_IP_DETECTION,
1701 array( Firewall_Analytics::PROP_IP_DETECTION => $detection_method )
1702 );
1703 }
1704
1705 /**
1706 * Upgrade.
1707 *
1708 * @return void
1709 */
1710 private function upgrade_5_0_0(): void {
1711 update_site_option( \WP_Defender\Component\IP\Antibot_Global_Firewall::NOTICE_SLUG, true );
1712 }
1713
1714 /**
1715 * Upgrade to 5.0.2: Clear the blocklist count. Also set the whitelist server public IP.
1716 *
1717 * @return void
1718 */
1719 private function upgrade_5_0_2(): void {
1720 delete_site_transient( 'wpdef_antibot_global_firewall_db_blocklist_count' );
1721 wd_di()->get( Firewall::class )->set_whitelist_server_public_ip();
1722 }
1723
1724 /**
1725 * Upgrade to 5.1.1: Migrate IP detection option to Automatic from Manual > All headers.
1726 *
1727 * @return void
1728 */
1729 private function upgrade_5_1_1(): void {
1730 $model = wd_di()->get( Model_Firewall::class );
1731 if ( 'manual' === $model->ip_detection_type && '' === $model->http_ip_header ) {
1732 $model->ip_detection_type = 'automatic';
1733 $model->http_ip_header = 'REMOTE_ADDR';
1734 $model->save();
1735 wd_di()->get( \WP_Defender\Component\Smart_Ip_Detection::class )->smart_ip_detection_ping();
1736 // Add tracking.
1737 $firewall_analytics = wd_di()->get( Firewall_Analytics::class );
1738 $detection_method = Firewall_Analytics::get_detection_method_label( 'automatic', '' );
1739
1740 $firewall_analytics->track_feature(
1741 Firewall_Analytics::EVENT_IP_DETECTION,
1742 array( Firewall_Analytics::PROP_IP_DETECTION => $detection_method )
1743 );
1744 }
1745 }
1746 /**
1747 * Update UA blocklist.
1748 *
1749 * @return void
1750 */
1751 private function update_ua_blocklist(): void {
1752 $settings = wd_di()->get( User_Agent_Lockout::class );
1753 $blacklist = $settings->get_lockout_list( 'blocklist', false );
1754 if ( empty( $blacklist ) ) {
1755 return;
1756 }
1757 $blacklist = array_filter(
1758 $blacklist,
1759 function ( $agent ) {
1760 return false === stripos( $agent, 'ahrefsbot' ) && false === stripos( $agent, 'semrushbot' );
1761 }
1762 );
1763 $settings->blacklist = implode( "\n", $blacklist ); // Convert back to string.
1764 $settings->save();
1765 }
1766
1767 /**
1768 * Upgrade to 5.2.0.
1769 *
1770 * @return void
1771 */
1772 private function upgrade_5_2_0(): void {
1773 $this->update_ua_blocklist();
1774 // Add the "What's new" modal.
1775 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1776 }
1777
1778 /**
1779 * Upgrade to 5.3.0.
1780 *
1781 * @return void
1782 */
1783 private function upgrade_5_3_0(): void {
1784 // Add the "What's new" modal.
1785 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1786 // Add composite index to the defender_lockout table.
1787 global $wpdb;
1788 // Check if the index already exists.
1789 $wpdb->query( "SHOW INDEX FROM {$wpdb->base_prefix}defender_lockout_log WHERE Key_name = 'idx_ip_date'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1790 if ( 0 === $wpdb->num_rows ) {
1791 // If the index does not exist, create it.
1792 $prev_val = $wpdb->hide_errors();
1793 $wpdb->query( "ALTER TABLE {$wpdb->base_prefix}defender_lockout_log ADD INDEX idx_ip_date (ip, date DESC);" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1794 $wpdb->show_errors( $prev_val );
1795 }
1796 }
1797
1798 /**
1799 * Upgrade to 5.3.1.
1800 *
1801 * @return void
1802 */
1803 private function upgrade_5_3_1(): void {
1804 delete_site_transient( \WP_Defender\Component\IP\Antibot_Global_Firewall::BLOCKLIST_STATS_KEY );
1805 }
1806
1807 /**
1808 * Improve UA Blocklist.
1809 *
1810 * @return void
1811 */
1812 private function improve_ua_blocklist(): void {
1813 $settings = wd_di()->get( User_Agent_Lockout::class );
1814 $blocklist_custom = $settings->get_lockout_list( 'blocklist' );
1815 if ( empty( $blocklist_custom ) ) {
1816 return;
1817 }
1818 // Get 'Blocklist Presets', check and remove duplicates on 'Custom User Agents'.
1819 $blocklist_presets = User_Agent_Service::get_nested_keys_of_blocklist_presets();
1820 $common_result = array_intersect( $blocklist_custom, $blocklist_presets );
1821 if ( ! empty( $common_result ) ) {
1822 $blocklist_custom = User_Agent_Service::check_and_remove_duplicates(
1823 $blocklist_custom,
1824 $common_result
1825 );
1826 // Convert back to string.
1827 $settings->blacklist = implode( PHP_EOL, $blocklist_custom );
1828 // Enable option with nested suboptions.
1829 $settings->blocklist_presets = true;
1830 $settings->blocklist_preset_values = $common_result;
1831 }
1832 // The same, but for 'Script Presets'.
1833 $script_presets = array_keys( User_Agent_Service::get_script_presets() );
1834 $common_result = array_intersect( $blocklist_custom, $script_presets );
1835 if ( ! empty( $common_result ) ) {
1836 $blocklist_custom = User_Agent_Service::check_and_remove_duplicates(
1837 $blocklist_custom,
1838 $common_result
1839 );
1840 // Convert back to string.
1841 $settings->blacklist = implode( PHP_EOL, $blocklist_custom );
1842 // Enable option with nested suboptions.
1843 $settings->script_presets = true;
1844 $settings->script_preset_values = $common_result;
1845 }
1846 $settings->save();
1847 }
1848
1849 /**
1850 * Upgrade to 5.4.0.
1851 *
1852 * @return void
1853 */
1854 private function upgrade_5_4_0(): void {
1855 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1856
1857 $this->improve_ua_blocklist();
1858 }
1859
1860 /**
1861 * Upgrade to 5.5.0.
1862 *
1863 * @return void
1864 */
1865 private function upgrade_5_5_0(): void {
1866 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1867 }
1868
1869 /**
1870 * Change lockout log mentions from fake_bot to malicious_bot. Also move BotTrap settings.
1871 *
1872 * @return void
1873 */
1874 private function change_to_malicious_bot(): void {
1875 global $wpdb;
1876
1877 $table_name = $wpdb->base_prefix . 'defender_lockout_log';
1878
1879 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1880 $wpdb->prepare(
1881 "UPDATE $table_name SET type = %s, log = %s WHERE type = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1882 'malicious_bot',
1883 'Lockout occurred: Bot ignored robots.txt rules.',
1884 'bot_trap'
1885 )
1886 );
1887 // Clear schedule as the name has changed to 'wpdef_rotate_malicious_bot_secret_hash'.
1888 wp_clear_scheduled_hook( 'wpdef_rotate_bot_trap_secret_hash' );
1889 // Move the BotTrap settings to new Malicious Bot settings.
1890 $settings = wd_di()->get( User_Agent_Lockout::class );
1891 if ( isset( $settings->bot_trap_enabled ) ) {
1892 $settings->malicious_bot_enabled = $settings->bot_trap_enabled;
1893 $settings->malicious_bot_lockout_type = $settings->bot_trap_lockout_type;
1894 $settings->malicious_bot_lockout_duration = $settings->bot_trap_lockout_duration;
1895 $settings->malicious_bot_lockout_duration_unit = $settings->bot_trap_lockout_duration_unit;
1896 $settings->save();
1897 }
1898 }
1899
1900 /**
1901 * Upgrade to 5.6.0.
1902 *
1903 * @return void
1904 */
1905 private function upgrade_5_6_0(): void {
1906 $this->change_to_malicious_bot();
1907 // Add the "What's new" modal.
1908 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1909 }
1910
1911 /**
1912 * Upgrade to 5.7.0.
1913 *
1914 * @return void
1915 */
1916 private function upgrade_5_7_0(): void {
1917 // Remove old objects from ncm options.
1918 wd_di()->get( \WP_Defender\Component\Network_Cron_Manager::class )->remove_data();
1919 // Add the "What's new" modal.
1920 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1921 // Add Cloudflare challenge-platform to 404 whitelist.
1922 $this->force_nf_lockout_exclusions();
1923 // Remove the prev Breadcrumbs.
1924 wd_di()->get( \WP_Defender\Component\Breadcrumbs::class )->delete_previous_meta();
1925 }
1926 }
1927