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

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