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

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