PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.0
Defender Security – Malware Scanner, Login Security & Firewall v5.8.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / class-upgrader.php

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

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