PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/controller/class-main-setting.php +673 -189 5.4.0 → 6.3.0 View file →
@@ -11,17 +11,25 @@
11 11 use WP_Defender\Event;
12 12 use Calotes\Component\Request;
13 13 use Calotes\Component\Response;
14 14 use WP_Defender\Behavior\WPMUDEV;
15 +use WP_Defender\Controller\Notification;
15 16 use WP_Defender\Component\Backup_Settings;
16 17 use WP_Defender\Component\Config\Config_Adapter;
18 +use WP_Defender\Component\Config\Local_Config_Store;
19 +use WP_Defender\Component\Network_Cron_Manager;
20 +use WP_Defender\Traits\Defender_Dashboard_Client;
17 21 use WP_Defender\Component\Config\Config_Hub_Helper;
18 22 use WP_Defender\Model\Setting\Main_Setting as Model_Main_Setting;
23 +use WP_Filesystem_Base;
24 +use WP_Defender\Controller\Security_Headers;
25 +use WP_Defender\Controller\Firewall;
19 26
20 27 /**
21 28 * Methods for handling main settings.
22 29 */
23 30 class Main_Setting extends Event {
31 + use Defender_Dashboard_Client;
24 32
25 33 /**
26 34 * The slug identifier for this controller.
27 35 *
@@ -50,8 +58,15 @@
50 58 */
51 59 protected $wpmudev;
52 60
53 61 /**
62 + * Local configs store.
63 + *
64 + * @var Local_Config_Store
65 + */
66 + protected $local_store;
67 +
68 + /**
54 69 * The intention/nonce action of the current request.
55 70 *
56 71 * @since 4.0.0
57 72 * @var string
@@ -64,26 +79,43 @@
64 79 public function __construct() {
65 80 $this->register_page(
66 81 esc_html__( 'Settings', 'defender-security' ),
67 82 $this->slug,
68 - array(
69 - &$this,
70 - 'main_view',
71 - ),
83 + array( $this, 'main_view' ),
72 84 $this->parent_slug
73 85 );
74 86
75 87 // Internal cache.
76 - $this->model = new Model_Main_Setting();
77 - $this->service = wd_di()->get( Backup_Settings::class );
78 - $this->wpmudev = wd_di()->get( WPMUDEV::class );
88 + $this->model = new Model_Main_Setting();
89 + $this->service = wd_di()->get( Backup_Settings::class );
90 + $this->wpmudev = wd_di()->get( WPMUDEV::class );
91 + $this->local_store = new Local_Config_Store( $this->service );
79 92 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
80 93 $this->register_routes();
81 94
82 - // Add cron schedule to clean out outdated logs.
83 - add_action( 'wp_defender_clear_logs', array( $this, 'clear_logs' ) );
84 - add_action( 'admin_init', array( $this, 'check_cron_schedule' ) );
95 + /**
96 + * Add cron schedule to clean out outdated logs.
97 + *
98 + * @var Network_Cron_Manager $network_cron_manager
99 + */
100 + $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
101 + $network_cron_manager->register_callback(
102 + 'wp_defender_clear_logs',
103 + array( $this, 'clear_logs' ),
104 + DAY_IN_SECONDS,
105 + time() + HOUR_IN_SECONDS
106 + );
85 107 add_action( 'wd_settings_update', array( $this, 'intercept_settings_update' ), 10, 2 );
108 + // Initialize Security Headers so its routes are registered on every request,
109 + // including AJAX calls from the Security Policies tab.
110 + wd_di()->get( Security_Headers::class );
111 + // Initialize Firewall so its routes (sync_ip_header, save_settings, empty_logs, etc.)
112 + // are registered on AJAX requests originating from the Tools tab.
113 + // Guarded to AJAX only to prevent a duplicate admin menu entry on normal page loads.
114 + if ( wp_doing_ajax() ) {
115 + wd_di()->get( Firewall::class );
116 + wd_di()->get( Audit_Logging::class );
117 + }
86 118 }
87 119
88 120 /**
89 121 * Check actual config data.
@@ -117,10 +149,73 @@
117 149 public function enqueue_assets() {
118 150 if ( ! $this->is_page_active() ) {
119 151 return;
120 152 }
121 - wp_localize_script( 'def-settings', 'settings', $this->data_frontend() );
122 - wp_enqueue_script( 'def-settings' );
153 +
154 + $handle = 'defender-ui-settings';
155 + wp_enqueue_script(
156 + $handle,
157 + WP_DEFENDER_BASE_URL . 'assets/js/settings-ui.js',
158 + array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
159 + DEFENDER_VERSION,
160 + true
161 + );
162 + wp_set_script_translations( $handle, 'wpdef' );
163 +
164 + $data = $this->data_frontend();
165 + $routes = $data['routes'] ?? array();
166 + $nonces = $data['nonces'] ?? array();
167 + unset( $data['routes'], $data['nonces'] );
168 +
169 + // Include Security Headers data for the Security Policies tab.
170 + $sh_controller = wd_di()->get( Security_Headers::class );
171 + $sh_data = $sh_controller->data_frontend();
172 + $sh_routes = $sh_data['routes'] ?? array();
173 + $sh_nonces = $sh_data['nonces'] ?? array();
174 + unset( $sh_data['routes'], $sh_data['nonces'] );
175 +
176 + // Namespace security headers routes/nonces to avoid key collisions.
177 + foreach ( $sh_routes as $key => $route ) {
178 + $routes[ 'security_headers_' . $key ] = $route;
179 + }
180 + foreach ( $sh_nonces as $key => $nonce ) {
181 + $nonces[ 'security_headers_' . $key ] = $nonce;
182 + }
183 +
184 + // Include Module data for the Tools tab.
185 + $scan_controller = wd_di()->get( Scan::class );
186 + $firewall_controller = wd_di()->get( Firewall::class );
187 + $audit_logging_controller = wd_di()->get( Audit_Logging::class );
188 + $blocklist_monitor_controller = wd_di()->get( Blocklist_Monitor::class );
189 + $notification_controller = wd_di()->get( Notification::class );
190 + $recipients_controller = wd_di()->get( Recipients::class );
191 + wp_localize_script(
192 + $handle,
193 + 'defenderUIData',
194 + array_merge(
195 + $this->get_shared_data(),
196 + // Specific data.
197 + array(
198 + 'settings' => array_merge( $data, array( 'security_headers' => $sh_data ) ),
199 + 'scan' => $scan_controller->data_frontend(),
200 + 'firewall' => $firewall_controller->data_frontend(),
201 + 'audit_logging' => $audit_logging_controller->settings_data_frontend(),
202 + 'blocklist_monitor' => $blocklist_monitor_controller->data_frontend(),
203 + 'notification' => $notification_controller->data_frontend(),
204 + 'recipients' => $recipients_controller->data_frontend(),
205 + 'routes' => $routes,
206 + 'nonces' => $nonces,
207 + )
208 + )
209 + );
210 +
211 + wp_enqueue_style(
212 + $handle,
213 + WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css',
214 + array(),
215 + DEFENDER_VERSION
216 + );
217 +
123 218 $this->enqueue_main_assets();
124 219 }
125 220
126 221 /**
@@ -173,10 +268,12 @@
173 268 * @return Response
174 269 * @defender_route
175 270 */
176 271 public function reset_settings(): Response {
177 - wd_di()->get( Advanced_Tools::class )->remove_settings();
178 - wd_di()->get( Audit_Logging::class )->remove_settings();
272 + $preserve_settings = 'preserve' === $this->get_model()->uninstall_settings;
273 +
274 + wd_di()->get( Login_Access::class )->remove_settings();
275 + wd_di()->get( Blocklist_Monitor::class )->remove_settings();
179 276 wd_di()->get( Dashboard::class )->remove_settings();
180 277 wd_di()->get( Security_Tweaks::class )->remove_settings();
181 278 wd_di()->get( Scan::class )->remove_settings();
182 279 // Parent and submodules.
@@ -185,12 +282,19 @@
185 282 wd_di()->get( Mask_Login::class )->remove_settings();
186 283 wd_di()->get( Notification::class )->remove_settings();
187 284 wd_di()->get( Two_Factor::class )->remove_settings();
188 285 wd_di()->get( Blocklist_Monitor::class )->remove_settings();
286 + wd_di()->get( Data_Tracking::class )->remove_settings();
287 + wd_di()->get( \WP_Defender\Controller\Setup_Wizard::class )->remove_settings();
288 + wd_di()->get( \WP_Defender\Controller\Activity_Log::class )->remove_settings();
289 +
189 290 $this->set_intention( 'Data Reset' );
190 291 // Track first until settings are removed.
191 292 $this->track_opt( false );
192 293 $this->remove_settings();
294 + if ( ! $preserve_settings ) {
295 + update_site_option( Scan::FIRST_SCAN_STARTED, '0' );
296 + }
193 297 // Indicate that it is not a new installation.
194 298 defender_no_fresh_install();
195 299
196 300 return new Response(
@@ -240,17 +344,10 @@
240 344 public function data_frontend(): array {
241 345 $model = $this->get_model();
242 346
243 347 $this->service->maybe_create_default_config();
244 - $configs = $this->get_configs_and_update_status();
348 + $configs = $this->local_store->get_frontend_configs();
245 349
246 - foreach ( $configs as &$config ) {
247 - // Unset the data as we don't need it.
248 - if ( isset( $config['configs'] ) ) {
249 - unset( $config['configs'] );
250 - }
251 - }
252 -
253 350 $link = $this->wpmudev->is_member()
254 351 ? 'https://wpmudev.com/translate/projects/wpdef/'
255 352 : 'https://translate.wordpress.org/projects/wp-plugins/defender-security/';
256 353 $can_whitelabel = wd_di()->get( \WP_Defender\Integrations\Dashboard_Whitelabel::class )->can_whitelabel();
@@ -275,8 +372,10 @@
275 372 'setting_url' => network_admin_url( is_multisite() ? 'settings.php' : 'options-general.php' ),
276 373 'privacy_link' => Model_Main_Setting::PRIVACY_LINK,
277 374 ),
278 375 'configs' => $configs,
376 + 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
377 + 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
279 378 ),
280 379 $this->dump_routes_and_nonces()
281 380 );
282 381 }
@@ -297,9 +396,14 @@
297 396 */
298 397 public function import_data( array $data ) {
299 398 $model = $this->get_model();
300 399
400 + // Preserve Usage Tracking; it is a user privacy choice that a config must not change.
401 + $current_usage_tracking = $model->usage_tracking;
402 +
301 403 $model->import( $data );
404 + $model->usage_tracking = $current_usage_tracking;
405 +
302 406 if ( $model->validate() ) {
303 407 $model->save();
304 408 }
305 409 }
@@ -313,29 +417,29 @@
313 417 *
314 418 * @return bool Returns true if the importer data is valid, false otherwise.
315 419 */
316 420 private function validate_importer( $importer ): bool {
317 - if ( $this->service->verify_config_data( $importer ) ) {
318 - // Validate content. This is the current data, we use this for verify the schema.
319 - $sample = $this->service->gather_data();
320 - foreach ( $importer['configs'] as $slug => $module ) {
321 - // This is not in the sample, file is invalid.
322 - if ( ! isset( $sample[ $slug ] ) ) {
323 - return false;
324 - }
421 + if ( ! $this->service->verify_config_data( $importer ) || ! is_array( $importer['configs'] ) ) {
422 + return false;
423 + }
325 424
326 - $keys = array_keys( $sample[ $slug ] );
327 - $import_keys = array_keys( $module );
328 - $diff = array_diff( $import_keys, $keys );
329 - if ( count( $diff ) ) {
330 - return false;
331 - }
425 + $sample = $this->service->gather_data();
426 + foreach ( $importer['configs'] as $slug => $module ) {
427 + // Ignore whole modules unavailable on this site, such as Pro-only modules in Free builds.
428 + if ( ! isset( $sample[ $slug ] ) ) {
429 + continue;
430 + }
332 431
333 - return true;
432 + if (
433 + ! is_array( $module )
434 + || ! is_array( $sample[ $slug ] )
435 + || array() !== array_diff_key( $module, $sample[ $slug ] )
436 + ) {
437 + return false;
334 438 }
335 439 }
336 440
337 - return false;
441 + return true;
338 442 }
339 443
340 444 /**
341 445 * Import config.
@@ -345,9 +449,9 @@
345 449 */
346 450 public function import_config(): Response {
347 451 global $wp_filesystem;
348 452 // Initialize the WP filesystem, no more using 'file-put-contents' function.
349 - if ( empty( $wp_filesystem ) ) {
453 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
350 454 require_once ABSPATH . '/wp-admin/includes/file.php';
351 455 WP_Filesystem();
352 456 }
353 457 $file = defender_get_data_from_request( 'file', 'f' );
@@ -362,14 +466,8 @@
362 466 )
363 467 );
364 468 }
365 469
366 - // If it's old config structure then we upgrade configs to new format.
367 - if ( ! empty( $importer['configs'] ) && ! $this->service->check_for_new_structure( $importer['configs'] ) ) {
368 - $adapter = wd_di()->get( Config_Adapter::class );
369 - $importer['configs'] = $adapter->upgrade( $importer['configs'] );
370 - }
371 -
372 470 if ( ! $this->validate_importer( $importer ) ) {
373 471 return new Response(
374 472 false,
375 473 array(
@@ -381,37 +479,70 @@
381 479 );
382 480 }
383 481
384 482 // Do not use wp_strip_all_tags() to prevent XSS attack.
385 - $name = sanitize_text_field( $importer['name'] );
386 - $configs = array(
387 - 'name' => $name,
388 - 'immortal' => false,
389 - 'is_removable' => true,
483 + $name = sanitize_text_field( $importer['name'] );
484 + $this->local_store->add_imported( $importer );
485 +
486 + // Detect Pro-only modules that were skipped during validation
487 + // to inform Free users that some settings were not imported.
488 + $response_data = array(
489 + 'message' => sprintf(
490 + /* translators: %s: Config name. */
491 + esc_html__(
492 + '%s config has been uploaded successfully – you can now apply it to this site.',
493 + 'defender-security'
494 + ),
495 + '<strong>' . $name . '</strong>'
496 + ),
497 + 'configs' => $this->local_store->get_frontend_configs(),
390 498 );
391 499
392 - $configs['configs'] = $importer['configs'];
393 - $configs['description'] = isset( $importer['description'] ) && ! empty( $importer['description'] )
394 - ? sanitize_textarea_field( $importer['description'] )
395 - : '';
396 - $configs['strings'] = $this->service->import_module_strings( $importer );
397 - $key = 'wp_defender_config_import_' . time();
398 - update_site_option( $key, $configs );
399 - $this->service->index_key( $key );
500 + if ( ! $this->wpmudev->is_pro() ) {
501 + $sample = $this->service->gather_data();
502 + $pro_modules = array();
400 503
401 - return new Response(
402 - true,
403 - array(
404 - 'message' => sprintf(
405 - /* translators: %s: Config name. */
504 + foreach ( $importer['configs'] as $slug => $module ) {
505 + // Modules present in the imported config but NOT in Free's sample data are Pro-only.
506 + if ( ! isset( $sample[ $slug ] ) ) {
507 + $pro_modules[] = $slug;
508 + }
509 + }
510 +
511 + if ( array() !== $pro_modules ) {
512 + $module_names = array_map(
513 + function ( $slug ) {
514 + $names = array(
515 + 'audit' => esc_html__( 'Audit Logs', 'defender-security' ),
516 + 'waf' => esc_html__( 'Web Application Firewall (WAF)', 'defender-security' ),
517 + 'blocklist_monitor' => esc_html__( 'Blocklist Monitor', 'defender-security' ),
518 + 'pwned_passwords' => esc_html__( 'Pwned Passwords', 'defender-security' ),
519 + 'force_strong_password' => esc_html__( 'Strong Passwords', 'defender-security' ),
520 + 'session_protection' => esc_html__( 'Session Protection', 'defender-security' ),
521 + 'two_factor' => esc_html__( 'Two-Factor Authentication', 'defender-security' ),
522 + 'mask_login' => esc_html__( 'Hide Login URL', 'defender-security' ),
523 + 'security_headers' => esc_html__( 'Security Policies', 'defender-security' ),
524 + );
525 +
526 + return $names[ $slug ] ?? $slug;
527 + },
528 + $pro_modules
529 + );
530 +
531 + $response_data['message'] .= ' ' . sprintf(
532 + /* translators: %s: Comma-separated list of Pro-only feature names. */
406 533 esc_html__(
407 - '%s config has been uploaded successfully – you can now apply it to this site.',
534 + 'This config included features not available in the Free version: %s. Only the available settings were imported.',
408 535 'defender-security'
409 536 ),
410 - '<strong>' . $name . '</strong>'
411 - ),
412 - 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
413 - )
537 + implode( ', ', $module_names )
538 + );
539 + }
540 + }
541 +
542 + return new Response(
543 + true,
544 + $response_data
414 545 );
415 546 }
416 547
417 548 /**
@@ -424,9 +555,9 @@
424 555 */
425 556 public function new_config( Request $request ): Response {
426 557 $data = $request->get_data();
427 558 $name = trim( $data['name'] );
428 - if ( empty( $name ) ) {
559 + if ( '' === $name ) {
429 560 return new Response(
430 561 false,
431 562 array(
432 563 'message' => esc_html__( 'Invalid config name', 'defender-security' ),
@@ -432,95 +563,196 @@
432 563 'message' => esc_html__( 'Invalid config name', 'defender-security' ),
433 564 )
434 565 );
435 566 }
436 - $name = sanitize_text_field( $name );
437 - $desc = isset( $data['desc'] ) && ! empty( $data['desc'] ) ? wp_kses_post( $data['desc'] ) : '';
438 - $key = 'wp_defender_config_' . time();
439 - $settings = $this->service->parse_data_for_import();
440 - $data = array_merge(
567 + $name = sanitize_text_field( $name );
568 + $desc = '';
569 + if ( isset( $data['desc'] ) && is_string( $data['desc'] ) && '' !== trim( $data['desc'] ) ) {
570 + $desc = wp_kses_post( $data['desc'] );
571 + } elseif ( isset( $data['description'] ) && is_string( $data['description'] ) && '' !== trim( $data['description'] ) ) {
572 + $desc = wp_kses_post( $data['description'] );
573 + }
574 + $note_added_time = isset( $data['note_added_time'] ) && is_scalar( $data['note_added_time'] )
575 + ? sanitize_text_field( (string) $data['note_added_time'] )
576 + : '';
577 + $this->local_store->create_from_current( $name, $desc, $note_added_time );
578 +
579 + return new Response(
580 + true,
441 581 array(
442 - 'name' => $name,
443 - 'immortal' => false,
444 - 'description' => $desc,
445 - 'is_removable' => true,
446 - ),
447 - $settings
582 + 'message' => sprintf(
583 + /* translators: %s: Config name. */
584 + esc_html__( '%s config saved successfully.', 'defender-security' ),
585 + '<strong>' . $name . '</strong>'
586 + ),
587 + 'configs' => $this->local_store->get_frontend_configs(),
588 + )
448 589 );
590 + }
449 591
450 - // Add config to HUB.
451 - $hub_id = Config_Hub_Helper::add_configs_to_hub( $data );
452 -
453 - if ( $hub_id ) {
454 - $data['hub_id'] = $hub_id;
455 - }
456 -
457 - unset( $data['labels'] );
458 -
459 - if ( update_site_option( $key, $data ) ) {
460 - $this->service->index_key( $key );
461 -
592 + /**
593 + * Download config
594 + *
595 + * @return Response|void
596 + * @defender_route
597 + */
598 + public function download_config() {
599 + $key = defender_get_data_from_request( 'key', 'g' );
600 + if ( ! is_string( $key ) || '' === trim( $key ) ) {
462 601 return new Response(
463 - true,
602 + false,
464 603 array(
465 - 'message' => sprintf(
466 - /* translators: %s: Config name. */
467 - esc_html__( '%s config saved successfully.', 'defender-security' ),
468 - '<strong>' . $name . '</strong>'
469 - ),
470 - 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
604 + 'message' => esc_html__( 'Invalid config', 'defender-security' ),
471 605 )
472 606 );
473 - } else {
607 + }
608 +
609 + $config = $this->local_store->get_full_for_key( $key );
610 + if ( null === $config ) {
611 + $config = $this->prepare_config_for_download( $key );
612 + }
613 + if ( null === $config ) {
474 614 return new Response(
475 615 false,
476 616 array(
477 - 'message' => esc_html__(
478 - 'An error occurred while saving your config. Please try it again.',
479 - 'defender-security'
480 - ),
617 + 'message' => esc_html__( 'Invalid config', 'defender-security' ),
481 618 )
482 619 );
483 620 }
621 +
622 + $filename = 'wp-defender-config-' . sanitize_file_name( $config['name'] ) . '.json';
623 + header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
624 + echo wp_json_encode( $config, JSON_PRETTY_PRINT );
625 + exit();
484 626 }
485 627
486 628 /**
487 - * Download config
629 + * Download multiple configs as a zip file.
488 630 *
489 631 * @return Response|void
490 632 * @defender_route
491 633 */
492 - public function download_config() {
493 - $key = defender_get_data_from_request( 'key', 'g' );
494 - if ( empty( $key ) ) {
634 + public function download_configs_zip() {
635 + $keys_raw = defender_get_data_from_request( 'keys', 'g' );
636 + if ( ! is_string( $keys_raw ) || '' === trim( $keys_raw ) ) {
495 637 return new Response(
496 638 false,
497 639 array(
498 - 'message' => esc_html__( 'Invalid config', 'defender-security' ),
640 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
499 641 )
500 642 );
501 643 }
502 644
503 - $config = get_site_option( $key );
504 - if ( false === $config ) {
645 + $decoded = json_decode( wp_unslash( $keys_raw ), true );
646 + $keys = is_array( $decoded ) ? $decoded : explode( ',', $keys_raw );
647 + $keys = array_values(
648 + array_filter(
649 + array_map(
650 + static function ( $key ) {
651 + return is_string( $key ) ? trim( $key ) : '';
652 + },
653 + $keys
654 + ),
655 + 'boolval'
656 + )
657 + );
658 +
659 + if ( array() === $keys ) {
505 660 return new Response(
506 661 false,
507 662 array(
508 - 'message' => esc_html__( 'Invalid config', 'defender-security' ),
663 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
509 664 )
510 665 );
511 666 }
512 - $sample = $this->service->gather_data();
513 - foreach ( $sample as $slug => $data ) {
514 - foreach ( $data as $key => $val ) {
515 - if ( ! isset( $config['configs'][ $slug ][ $key ] ) ) {
516 - $config['configs'][ $slug ][ $key ] = null;
517 - }
667 +
668 + if ( ! class_exists( 'ZipArchive' ) ) {
669 + return new Response(
670 + false,
671 + array(
672 + 'message' => esc_html__( 'Zip archive is not supported on this server.', 'defender-security' ),
673 + )
674 + );
675 + }
676 +
677 + $tmp_zip_path = wp_tempnam( 'wp-defender-configs-' . time() . '.zip' );
678 + if ( false === $tmp_zip_path ) {
679 + return new Response(
680 + false,
681 + array(
682 + 'message' => esc_html__( 'Could not prepare zip file.', 'defender-security' ),
683 + )
684 + );
685 + }
686 +
687 + $cleanup_tmp_zip = static function () use ( $tmp_zip_path ) {
688 + if ( file_exists( $tmp_zip_path ) ) {
689 + wp_delete_file( $tmp_zip_path );
518 690 }
691 + };
692 +
693 + $zip = new \ZipArchive();
694 + if ( true !== $zip->open( $tmp_zip_path, \ZipArchive::OVERWRITE ) ) {
695 + $cleanup_tmp_zip();
696 +
697 + return new Response(
698 + false,
699 + array(
700 + 'message' => esc_html__( 'Could not create zip file.', 'defender-security' ),
701 + )
702 + );
519 703 }
520 - $filename = 'wp-defender-config-' . sanitize_file_name( $config['name'] ) . '.json';
521 - header( 'Content-disposition: attachment; filename=' . $filename );
522 - echo wp_json_encode( $config, JSON_PRETTY_PRINT );
704 +
705 + $has_entries = false;
706 +
707 + foreach ( $keys as $key ) {
708 + $config = $this->local_store->get_full_for_key( $key );
709 + if ( null === $config ) {
710 + $config = $this->prepare_config_for_download( $key );
711 + }
712 + if ( null === $config ) {
713 + continue;
714 + }
715 +
716 + $file_name = $this->make_unique_zip_entry_name(
717 + 'wp-defender-config-' . sanitize_file_name( $config['name'] ),
718 + $key,
719 + $zip
720 + );
721 + if ( false === $zip->addFromString( $file_name, wp_json_encode( $config, JSON_PRETTY_PRINT ) ) ) {
722 + $zip->close();
723 + $cleanup_tmp_zip();
724 +
725 + return new Response(
726 + false,
727 + array(
728 + 'message' => esc_html__( 'Could not prepare zip file.', 'defender-security' ),
729 + )
730 + );
731 + }
732 +
733 + $has_entries = true;
734 + }
735 +
736 + $zip->close();
737 +
738 + if ( ! $has_entries ) {
739 + $cleanup_tmp_zip();
740 +
741 + return new Response(
742 + false,
743 + array(
744 + 'message' => esc_html__( 'No valid configs found to export.', 'defender-security' ),
745 + )
746 + );
747 + }
748 +
749 + $download_name = 'wp-defender-configs-' . gmdate( 'Ymd-His' ) . '.zip';
750 + header( 'Content-Type: application/zip' );
751 + header( 'Content-Disposition: attachment; filename="' . $download_name . '"' );
752 + header( 'Content-Length: ' . filesize( $tmp_zip_path ) );
753 + readfile( $tmp_zip_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
754 + $cleanup_tmp_zip();
523 755 exit();
524 756 }
525 757
526 758 /**
@@ -533,9 +765,9 @@
533 765 */
534 766 public function apply_config( Request $request ) {
535 767 $data = $request->get_data();
536 768 $key = trim( $data['key'] );
537 - if ( empty( $key ) ) {
769 + if ( '' === $key ) {
538 770 return new Response(
539 771 false,
540 772 array(
541 773 'message' => esc_html__( 'Invalid config', 'defender-security' ),
@@ -542,10 +774,13 @@
542 774 )
543 775 );
544 776 }
545 777
546 - $config = get_site_option( $key );
547 - if ( false === $config ) {
778 + $config = $this->local_store->get_full_for_key( $key );
779 + if ( null === $config ) {
780 + $config = get_site_option( $key );
781 + }
782 + if ( false === $config || null === $config ) {
548 783 return new Response(
549 784 false,
550 785 array(
551 786 'message' => esc_html__( 'Invalid config', 'defender-security' ),
@@ -557,8 +792,9 @@
557 792 if ( is_string( $restore_result ) ) {
558 793 return $this->apply_config_recommendations_error_message();
559 794 }
560 795
796 + $this->local_store->apply( $key );
561 797 $this->service->make_config_active( $key );
562 798 // Track.
563 799 $this->track_feature(
564 800 'def_config_applied',
@@ -568,9 +804,9 @@
568 804 )
569 805 );
570 806
571 807 $message = sprintf(
572 - /* translators: %s: Config name. */
808 + /* translators: %s: Config name. */
573 809 esc_html__(
574 810 '%s config has been applied successfully.',
575 811 'defender-security'
576 812 ),
@@ -583,9 +819,9 @@
583 819 if ( $settings_mask_login->is_active() ) {
584 820 $login_url = $settings_mask_login->get_new_login_url();
585 821 }
586 822 $message .= '<br/>' . sprintf(
587 - /* translators: %s: Login link. */
823 + /* translators: %s: Login link. */
588 824 esc_html__(
589 825 'Due to currently applied security recommendations, you will now need to %s.',
590 826 'defender-security'
591 827 ),
@@ -608,9 +844,9 @@
608 844 }
609 845
610 846 $return['message'] = $message;
611 847 $return['auto_close'] = true;
612 - $return['configs'] = Config_Hub_Helper::get_fresh_frontend_configs( $this->service );
848 + $return['configs'] = $this->local_store->get_frontend_configs();
613 849
614 850 return new Response( true, $return );
615 851 }
616 852
@@ -624,11 +860,18 @@
624 860 */
625 861 public function update_config( Request $request ) {
626 862 $data = $request->get_data();
627 863 $key = trim( $data['key'] );
864 + $key = $this->resolve_config_key( $key );
628 865 $name = trim( $data['name'] );
629 - $description = trim( $data['desc'] );
630 - if ( empty( $name ) || empty( $key ) ) {
866 + $description = isset( $data['desc'] ) ? trim( (string) $data['desc'] ) : '';
867 + if ( '' === $description && isset( $data['description'] ) ) {
868 + $description = trim( (string) $data['description'] );
869 + }
870 + $note_added_time = isset( $data['note_added_time'] ) && is_scalar( $data['note_added_time'] )
871 + ? sanitize_text_field( (string) $data['note_added_time'] )
872 + : null;
873 + if ( '' === $name || '' === $key ) {
631 874 return new Response(
632 875 false,
633 876 array(
634 877 'message' => esc_html__( 'Invalid config', 'defender-security' ),
@@ -635,10 +878,13 @@
635 878 )
636 879 );
637 880 }
638 881
639 - $config = get_site_option( $key );
640 - if ( false === $config ) {
882 + $config = $this->local_store->get_one( $key );
883 + if ( null === $config ) {
884 + $config = get_site_option( $key );
885 + }
886 + if ( false === $config || null === $config ) {
641 887 return new Response(
642 888 false,
643 889 array(
644 890 'message' => esc_html__( 'Invalid config', 'defender-security' ),
@@ -645,22 +891,17 @@
645 891 )
646 892 );
647 893 }
648 894
649 - $old_config = $config;
650 - $config['name'] = sanitize_text_field( $name );
651 - $config['description'] = sanitize_textarea_field( $description );
652 -
653 - // Check data has been changed or not.
654 - if (
655 - $old_config['name'] === $config['name'] &&
656 - $old_config['description'] === $config['description']
657 - ) {
658 - // Data is not changed, so not need to run update query.
659 - $option_updated = true;
660 - } else {
895 + $option_updated = $this->local_store->update_metadata( $key, $name, $description, $note_added_time );
896 + if ( ! $option_updated ) {
897 + $old_config = $config;
898 + $config['name'] = sanitize_text_field( $name );
899 + $config['description'] = sanitize_textarea_field( $description );
900 + if ( null !== $note_added_time ) {
901 + $config['note_added_time'] = $note_added_time;
902 + }
661 903 $option_updated = update_site_option( $key, $config );
662 - Config_Hub_Helper::update_on_hub( $config );
663 904 }
664 905
665 906 if ( $option_updated ) {
666 907 return new Response(
@@ -666,14 +907,14 @@
666 907 return new Response(
667 908 true,
668 909 array(
669 910 'message' => sprintf(
670 - /* translators: %s: Config name. */
911 + /* translators: %s: Config name. */
671 912 esc_html__( '%s config saved successfully.', 'defender-security' ),
672 913 '<strong>' . $name . '</strong>'
673 914 ),
674 915 'auto_close' => true,
675 - 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
916 + 'configs' => $this->local_store->get_frontend_configs(),
676 917 )
677 918 );
678 919 } else {
679 920 return new Response(
@@ -688,8 +929,100 @@
688 929 }
689 930 }
690 931
691 932 /**
933 + * Sync config metadata list.
934 + *
935 + * This method updates name/description/note timestamp for provided configs
936 + * without changing config payload values.
937 + *
938 + * @param Request $request The request object containing configs metadata list.
939 + *
940 + * @return Response
941 + * @defender_route
942 + */
943 + public function sync_configs_metadata( Request $request ): Response {
944 + // Defense in depth: central routing already validates nonce and access.
945 + if ( ! $this->check_permission() ) {
946 + return new Response(
947 + false,
948 + array(
949 + 'message' => esc_html__( 'You shall not pass.', 'defender-security' ),
950 + )
951 + );
952 + }
953 +
954 + $data = $request->get_data();
955 + if ( ! isset( $data['configs'] ) || ! is_array( $data['configs'] ) ) {
956 + return new Response(
957 + false,
958 + array(
959 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
960 + )
961 + );
962 + }
963 +
964 + $stored_configs = $this->local_store->get_all();
965 + foreach ( $data['configs'] as $item ) {
966 + if ( ! is_array( $item ) ) {
967 + return new Response(
968 + false,
969 + array(
970 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
971 + )
972 + );
973 + }
974 +
975 + $key = '';
976 + if ( isset( $item['key'] ) && is_string( $item['key'] ) ) {
977 + $key = trim( $item['key'] );
978 + } elseif ( isset( $item['id'] ) && is_string( $item['id'] ) ) {
979 + $key = trim( $item['id'] );
980 + }
981 + $key = $this->resolve_config_key( $key, $stored_configs );
982 +
983 + if (
984 + '' === $key ||
985 + 0 !== strpos( $key, 'wp_defender_config' ) ||
986 + ! isset( $stored_configs[ $key ] )
987 + ) {
988 + return new Response(
989 + false,
990 + array(
991 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
992 + )
993 + );
994 + }
995 +
996 + $config = $this->local_store->get_one( $key );
997 + if ( null === $config ) {
998 + continue;
999 + }
1000 +
1001 + $name = isset( $item['name'] ) ? (string) $item['name'] : null;
1002 + $description = null;
1003 + $note_added_time = null;
1004 + if ( isset( $item['description'] ) && is_string( $item['description'] ) ) {
1005 + $description = $item['description'];
1006 + } elseif ( isset( $item['desc'] ) && is_string( $item['desc'] ) ) {
1007 + $description = $item['desc'];
1008 + }
1009 + if ( isset( $item['note_added_time'] ) && is_scalar( $item['note_added_time'] ) ) {
1010 + $note_added_time = (string) $item['note_added_time'];
1011 + }
1012 + $this->local_store->update_metadata( $key, $name, $description, $note_added_time );
1013 + }
1014 +
1015 + return new Response(
1016 + true,
1017 + array(
1018 + 'message' => esc_html__( 'Configs synced successfully.', 'defender-security' ),
1019 + 'configs' => $this->local_store->get_frontend_configs(),
1020 + )
1021 + );
1022 + }
1023 +
1024 + /**
692 1025 * Delete config.
693 1026 *
694 1027 * @param Request $request The request object containing config key.
695 1028 *
@@ -698,9 +1031,10 @@
698 1031 */
699 1032 public function delete_config( Request $request ) {
700 1033 $data = $request->get_data();
701 1034 $key = trim( $data['key'] );
702 - if ( empty( $key ) ) {
1035 + $key = $this->resolve_config_key( $key, $this->local_store->get_all() );
1036 + if ( '' === $key ) {
703 1037 return new Response(
704 1038 false,
705 1039 array(
706 1040 'message' => esc_html__( 'Invalid config', 'defender-security' ),
@@ -707,10 +1041,13 @@
707 1041 )
708 1042 );
709 1043 }
710 1044
711 - $config = get_site_option( $key );
712 - if ( isset( $config['is_removable'] ) && ! $config['is_removable'] ) {
1045 + $config = $this->local_store->get_one( $key );
1046 + if ( null === $config ) {
1047 + $config = get_site_option( $key );
1048 + }
1049 + if ( isset( $config['is_removable'] ) && false === (bool) $config['is_removable'] ) {
713 1050 return new Response(
714 1051 false,
715 1052 array(
716 1053 'message' => esc_html__( 'Config can\'t be removed', 'defender-security' ),
@@ -717,16 +1054,24 @@
717 1054 )
718 1055 );
719 1056 }
720 1057
721 - // Remove from HUB.
722 - if ( isset( $config['hub_id'] ) ) {
723 - Config_Hub_Helper::delete_configs_from_hub( (int) $config['hub_id'] );
1058 + if ( $this->local_store->delete_one( $key ) ) {
1059 + return new Response(
1060 + true,
1061 + array(
1062 + 'message' => esc_html__( 'Config removed successfully.', 'defender-security' ),
1063 + 'auto_close' => true,
1064 + 'configs' => $this->local_store->get_frontend_configs(),
1065 + )
1066 + );
724 1067 }
725 1068
726 1069 if ( 0 === strpos( $key, 'wp_defender_config' ) ) {
727 1070 delete_site_option( $key );
1071 + $this->service->remove_index( $key );
728 1072 $this->service->clear_keys();
1073 + delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
729 1074
730 1075 return new Response(
731 1076 true,
732 1077 array(
@@ -731,9 +1076,9 @@
731 1076 true,
732 1077 array(
733 1078 'message' => esc_html__( 'Config removed successfully.', 'defender-security' ),
734 1079 'auto_close' => true,
735 - 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
1080 + 'configs' => $this->local_store->get_frontend_configs(),
736 1081 )
737 1082 );
738 1083 }
739 1084
@@ -745,8 +1090,85 @@
745 1090 );
746 1091 }
747 1092
748 1093 /**
1094 + * Syncs configs list by keeping only the provided config keys.
1095 + *
1096 + * This method is intentionally separate from `delete_config` for backward compatibility.
1097 + *
1098 + * @param Request $request The request object containing the list of configs to keep.
1099 + *
1100 + * @return Response
1101 + * @defender_route
1102 + */
1103 + public function sync_configs_list( Request $request ): Response {
1104 + // Defense in depth: central routing already enforces nonce and private access checks.
1105 + if ( ! $this->check_permission() ) {
1106 + return new Response(
1107 + false,
1108 + array(
1109 + 'message' => esc_html__( 'You shall not pass.', 'defender-security' ),
1110 + )
1111 + );
1112 + }
1113 +
1114 + $data = $request->get_data();
1115 + if ( ! isset( $data['configs'] ) || ! is_array( $data['configs'] ) ) {
1116 + return new Response(
1117 + false,
1118 + array(
1119 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
1120 + )
1121 + );
1122 + }
1123 +
1124 + $stored_configs = $this->local_store->get_all();
1125 + $keep_keys = array();
1126 + foreach ( $data['configs'] as $item ) {
1127 + if ( ! is_array( $item ) ) {
1128 + return new Response(
1129 + false,
1130 + array(
1131 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
1132 + )
1133 + );
1134 + }
1135 + $key = '';
1136 + if ( isset( $item['key'] ) && is_string( $item['key'] ) ) {
1137 + $key = trim( $item['key'] );
1138 + } elseif ( isset( $item['id'] ) && is_string( $item['id'] ) ) {
1139 + $key = trim( $item['id'] );
1140 + }
1141 +
1142 + if (
1143 + '' === $key ||
1144 + 0 !== strpos( $key, 'wp_defender_config' ) ||
1145 + ! isset( $stored_configs[ $key ] )
1146 + ) {
1147 + return new Response(
1148 + false,
1149 + array(
1150 + 'message' => esc_html__( 'Invalid config list', 'defender-security' ),
1151 + )
1152 + );
1153 + }
1154 +
1155 + $keep_keys[ $key ] = true;
1156 + }
1157 +
1158 + $this->local_store->sync_keep_list( array_keys( $keep_keys ) );
1159 +
1160 + return new Response(
1161 + true,
1162 + array(
1163 + 'message' => esc_html__( 'Config list synced successfully.', 'defender-security' ),
1164 + 'auto_close' => true,
1165 + 'configs' => $this->local_store->get_frontend_configs(),
1166 + )
1167 + );
1168 + }
1169 +
1170 + /**
749 1171 * Exports strings.
750 1172 *
751 1173 * @return array An array of strings.
752 1174 */
@@ -754,48 +1176,53 @@
754 1176 return array();
755 1177 }
756 1178
757 1179 /**
758 - * Update config status and return them.
1180 + * Resolves a config key from an incoming identifier.
759 1181 *
760 - * @return array
1182 + * Supports direct option keys and numeric Hub IDs.
1183 + *
1184 + * @param string $incoming_key Incoming key/id.
1185 + * @param array|null $stored_configs Optional preloaded configs map.
1186 + *
1187 + * @return string
761 1188 */
762 - private function get_configs_and_update_status() {
763 - $configs = Config_Hub_Helper::get_configs( $this->service );
764 - $is_remove = Config_Hub_Helper::check_remove_active_flag();
1189 + private function resolve_config_key( string $incoming_key, ?array $stored_configs = null ): string {
1190 + $key = trim( $incoming_key );
1191 + if ( '' === $key ) {
1192 + return '';
1193 + }
765 1194
766 - // Loop to update strings of configs.
767 - foreach ( $configs as $key => &$config ) {
768 - if ( ! is_array( $config ) ) {
1195 + if ( 0 === strpos( $key, 'wp_defender_config' ) ) {
1196 + return $key;
1197 + }
1198 +
1199 + $configs = is_array( $stored_configs ) ? $stored_configs : $this->local_store->get_all();
1200 + foreach ( $configs as $config_key => $config ) {
1201 + if ( ! is_array( $config ) || ! isset( $config['hub_id'] ) ) {
769 1202 continue;
770 1203 }
771 -
772 - $config['strings'] = $this->service->import_module_strings( $config );
773 -
774 - if ( $is_remove ) {
775 - $config['is_active'] = false;
1204 + if ( (string) $config['hub_id'] === $key ) {
1205 + return (string) $config_key;
776 1206 }
777 -
778 - // Update config data.
779 - update_site_option( $key, $config );
780 1207 }
781 1208
782 - return $configs;
1209 + return $key;
783 1210 }
784 1211
785 1212 /**
786 - * Generates an error message for when there is an issue applying some tweaks from the Recommendations tab.
1213 + * Generates an error message for when there is an issue applying some tweaks from the Hardening tab.
787 1214 *
788 1215 * @return Response The response object containing the error message and fresh frontend configurations.
789 1216 */
790 1217 private function apply_config_recommendations_error_message(): Response {
791 1218 $message = sprintf(
792 - /* translators: 1: Recommendations tab, 2: wp-config.php file, 3: Documentation. */
1219 + /* translators: 1: Hardening tab, 2: wp-config.php file, 3: Documentation. */
793 1220 esc_html__(
794 1221 'There was an issue with applying some of the tweaks from the %1$s tab because we cannot make changes to your %2$s file. Please see our %3$s to apply the changes manually.',
795 1222 'defender-security'
796 1223 ),
797 - '<strong>' . esc_html__( 'Recommendations', 'defender-security' ) . '</strong>',
1224 + '<strong>' . esc_html__( 'Hardening', 'defender-security' ) . '</strong>',
798 1225 '<strong>' . esc_html__( 'wp-config.php', 'defender-security' ) . '</strong>',
799 1226 '<a href="' . WP_DEFENDER_DOCS_LINK . '#manually-applying-recommendations" target="_blank">' . esc_html__( 'documentation', 'defender-security' ) . '</a>'
800 1227 );
801 1228
@@ -802,25 +1229,77 @@
802 1229 return new Response(
803 1230 false,
804 1231 array(
805 1232 'message' => $message,
806 - 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
1233 + 'configs' => $this->local_store->get_frontend_configs(),
807 1234 )
808 1235 );
809 1236 }
810 1237
811 1238 /**
812 - * Check if the logger cron is scheduled to run.
1239 + * Prepares a config payload for download.
813 1240 *
814 - * @return void
1241 + * @param string $key Config option key.
1242 + *
1243 + * @return array|null
815 1244 */
816 - public function check_cron_schedule(): void {
817 - if ( ! wp_next_scheduled( 'wp_defender_clear_logs' ) ) {
818 - wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wp_defender_clear_logs' );
1245 + private function prepare_config_for_download( string $key ): ?array {
1246 + $config = get_site_option( $key );
1247 + if ( false === $config || ! is_array( $config ) ) {
1248 + return null;
819 1249 }
1250 +
1251 + $sample = $this->service->gather_data();
1252 + foreach ( $sample as $slug => $data ) {
1253 + foreach ( $data as $k => $val ) {
1254 + if ( ! isset( $config['configs'][ $slug ][ $k ] ) ) {
1255 + $config['configs'][ $slug ][ $k ] = null;
1256 + }
1257 + }
1258 + }
1259 +
1260 + return $config;
820 1261 }
821 1262
822 1263 /**
1264 + * Creates a unique JSON filename for a zip entry.
1265 + *
1266 + * @param string $base_name Base file name without extension.
1267 + * @param string $key Config option key.
1268 + * @param \ZipArchive $zip Active zip archive.
1269 + *
1270 + * @return string
1271 + */
1272 + private function make_unique_zip_entry_name( string $base_name, string $key, \ZipArchive $zip ): string {
1273 + $base_name = trim( $base_name );
1274 + if ( '' === $base_name ) {
1275 + $base_name = 'wp-defender-config';
1276 + }
1277 +
1278 + $try_name = $base_name . '.json';
1279 + if ( false === $zip->locateName( $try_name ) ) {
1280 + return $try_name;
1281 + }
1282 +
1283 + $key_suffix = sanitize_file_name( str_replace( 'wp_defender_config_', '', $key ) );
1284 + if ( '' !== $key_suffix ) {
1285 + $try_name = $base_name . '-' . $key_suffix . '.json';
1286 + if ( false === $zip->locateName( $try_name ) ) {
1287 + return $try_name;
1288 + }
1289 + }
1290 +
1291 + $index = 2;
1292 + while ( true ) {
1293 + $try_name = $base_name . '-' . $index . '.json';
1294 + if ( false === $zip->locateName( $try_name ) ) {
1295 + return $try_name;
1296 + }
1297 + ++$index;
1298 + }
1299 + }
1300 +
1301 + /**
823 1302 * Clear out lines that are older than 30 days.
824 1303 *
825 1304 * @return void
826 1305 */
@@ -831,20 +1310,25 @@
831 1310 if ( is_multisite() ) {
832 1311 global $wpdb;
833 1312 $offset = 0;
834 1313 $limit = 100;
835 - // Variable within condition is for comparison.
836 - while ( $blogs = $wpdb->get_results( $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ), ARRAY_A ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
837 - if ( ! empty( $blogs ) && is_array( $blogs ) ) {
838 - foreach ( $blogs as $blog ) {
839 - switch_to_blog( $blog['blog_id'] );
1314 + $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1315 + $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
1316 + ARRAY_A
1317 + );
1318 + while ( is_array( $blogs ) && array() !== $blogs ) {
1319 + foreach ( $blogs as $blog ) {
1320 + switch_to_blog( $blog['blog_id'] );
840 1321
841 - $this->clear_logs_from_files( $time_limit );
1322 + $this->clear_logs_from_files( $time_limit );
842 1323
843 - restore_current_blog();
844 - }
1324 + restore_current_blog();
845 1325 }
846 1326 $offset += $limit;
1327 + $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1328 + $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
1329 + ARRAY_A
1330 + );
847 1331 }
848 1332 } else {
849 1333 $this->clear_logs_from_files( $time_limit );
850 1334 }
@@ -860,9 +1344,9 @@
860 1344 */
861 1345 public function clear_logs_from_files( int $time_limit = MONTH_IN_SECONDS ) {
862 1346 global $wp_filesystem;
863 1347 // Initialize the WP filesystem, no more using 'file-put-contents' function.
864 - if ( empty( $wp_filesystem ) ) {
1348 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
865 1349 require_once ABSPATH . '/wp-admin/includes/file.php';
866 1350 WP_Filesystem();
867 1351 }
868 1352 $now = wp_date( 'c' );
@@ -937,8 +1421,8 @@
937 1421 '' !== $from
938 1422 && isset( $new_settings['usage_tracking'], $old_settings['usage_tracking'] )
939 1423 && $new_settings['usage_tracking'] !== $old_settings['usage_tracking']
940 1424 ) {
941 - $this->track_opt_toggle( ! empty( $new_settings['usage_tracking'] ), $from );
1425 + $this->track_opt_toggle( (bool) $new_settings['usage_tracking'], $from );
942 1426 }
943 1427 }
944 1428 }