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

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