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

class-main-setting.php in Defender Security – Malware Scanner, Login Security & Firewall 5.9.0, at src/controller/class-main-setting.php

974 lines 25.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the main settings.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use Countable;
11 use WP_Defender\Event;
12 use Calotes\Component\Request;
13 use Calotes\Component\Response;
14 use WP_Defender\Behavior\WPMUDEV;
15 use WP_Defender\Component\Backup_Settings;
16 use WP_Defender\Component\Config\Config_Adapter;
17 use WP_Defender\Component\Network_Cron_Manager;
18 use WP_Defender\Traits\Defender_Dashboard_Client;
19 use WP_Defender\Component\Config\Config_Hub_Helper;
20 use WP_Defender\Model\Setting\Main_Setting as Model_Main_Setting;
21 use WP_Filesystem_Base;
22
23 /**
24 * Methods for handling main settings.
25 */
26 class Main_Setting extends Event {
27 use Defender_Dashboard_Client;
28
29 /**
30 * The slug identifier for this controller.
31 *
32 * @var string
33 */
34 public $slug = 'wdf-setting';
35
36 /**
37 * The model for handling the data.
38 *
39 * @var Model_Main_Setting
40 */
41 public $model;
42
43 /**
44 * Service for handling logic.
45 *
46 * @var Backup_Settings
47 */
48 protected $service;
49
50 /**
51 * Service for handling WPMUDEV.
52 *
53 * @var WPMUDEV
54 */
55 protected $wpmudev;
56
57 /**
58 * The intention/nonce action of the current request.
59 *
60 * @since 4.0.0
61 * @var string
62 */
63 protected $intention = '';
64
65 /**
66 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
67 */
68 public function __construct() {
69 $this->register_page(
70 esc_html__( 'Settings', 'defender-security' ),
71 $this->slug,
72 array( $this, 'main_view' ),
73 $this->parent_slug
74 );
75
76 // Internal cache.
77 $this->model = new Model_Main_Setting();
78 $this->service = wd_di()->get( Backup_Settings::class );
79 $this->wpmudev = wd_di()->get( WPMUDEV::class );
80 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
81 $this->register_routes();
82
83 /**
84 * Add cron schedule to clean out outdated logs.
85 *
86 * @var Network_Cron_Manager $network_cron_manager
87 */
88 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
89 $network_cron_manager->register_callback(
90 'wp_defender_clear_logs',
91 array( $this, 'clear_logs' ),
92 DAY_IN_SECONDS,
93 time() + HOUR_IN_SECONDS
94 );
95 add_action( 'wd_settings_update', array( $this, 'intercept_settings_update' ), 10, 2 );
96 }
97
98 /**
99 * Check actual config data.
100 *
101 * @return Response
102 * @defender_route
103 */
104 public function check_configs(): Response {
105 Config_Hub_Helper::clear_config_transient();
106
107 return new Response( true, array() );
108 }
109
110 /**
111 * Safe way to get cached model.
112 *
113 * @return Model_Main_Setting
114 */
115 private function get_model() {
116 if ( is_object( $this->model ) ) {
117 return $this->model;
118 }
119
120 return wd_di()->get( Model_Main_Setting::class );
121 }
122
123 /**
124 * Enqueues scripts and styles for this page.
125 * Only enqueues assets if the page is active.
126 */
127 public function enqueue_assets() {
128 if ( ! $this->is_page_active() ) {
129 return;
130 }
131 wp_localize_script( 'def-settings', 'settings', $this->data_frontend() );
132 wp_enqueue_script( 'def-settings' );
133 $this->enqueue_main_assets();
134 }
135
136 /**
137 * Render the root element for frontend.
138 *
139 * @return void
140 */
141 public function main_view(): void {
142 $this->render( 'main' );
143 }
144
145 /**
146 * Save settings.
147 *
148 * @param Request $request The request object containing new settings data.
149 *
150 * @return Response
151 * @defender_route
152 */
153 public function save_settings( Request $request ) {
154 $model = $this->get_model();
155 $data = $request->get_data();
156
157 $model->import( $data );
158 if ( $model->validate() ) {
159 $this->set_intention( 'Settings' );
160 $model->save();
161 Config_Hub_Helper::set_clear_active_flag();
162
163 return new Response(
164 true,
165 array(
166 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
167 'auto_close' => true,
168 )
169 );
170 }
171
172 return new Response(
173 false,
174 array(
175 'message' => $model->get_formatted_errors(),
176 )
177 );
178 }
179
180 /**
181 * Reset settings.
182 *
183 * @return Response
184 * @defender_route
185 */
186 public function reset_settings(): Response {
187 wd_di()->get( Advanced_Tools::class )->remove_settings();
188 wd_di()->get( Audit_Logging::class )->remove_settings();
189 wd_di()->get( Dashboard::class )->remove_settings();
190 wd_di()->get( Security_Tweaks::class )->remove_settings();
191 wd_di()->get( Scan::class )->remove_settings();
192 // Parent and submodules.
193 wd_di()->get( Firewall::class )->remove_settings();
194
195 wd_di()->get( Mask_Login::class )->remove_settings();
196 wd_di()->get( Notification::class )->remove_settings();
197 wd_di()->get( Two_Factor::class )->remove_settings();
198 wd_di()->get( Blocklist_Monitor::class )->remove_settings();
199 $this->set_intention( 'Data Reset' );
200 // Track first until settings are removed.
201 $this->track_opt( false );
202 $this->remove_settings();
203 // Indicate that it is not a new installation.
204 defender_no_fresh_install();
205
206 return new Response(
207 true,
208 array(
209 'message' => esc_html__( 'Your settings have been reset.', 'defender-security' ),
210 'redirect' => network_admin_url( 'admin.php?page=wp-defender' ),
211 'interval' => 1,
212 )
213 );
214 }
215
216 /**
217 * Tracks the settings toggle.
218 *
219 * @param bool $active The status of the toggle.
220 *
221 * @return void
222 */
223 public function track_opt( $active ) {
224 $model = $this->get_model();
225 // Track only if the Data tracking option was enabled before changes.
226 if ( $model->usage_tracking ) {
227 $from = $this->get_triggered_location();
228 $this->track_opt_toggle( $active, $from );
229 }
230 }
231
232 /**
233 * Removes settings for all submodules.
234 */
235 public function remove_settings(): void {
236 wd_di()->get( Model_Main_Setting::class )->delete();
237 }
238
239 /**
240 * Delete all the data & the cache.
241 */
242 public function remove_data() {
243 }
244
245 /**
246 * Disconnect site from HUB.
247 *
248 * @defender_route
249 * @return Response
250 */
251 public function disconnect_site(): Response {
252 $this->logout();
253
254 return new Response(
255 true,
256 array(
257 'message' => esc_html__( 'Site disconnected successfully!', 'defender-security' ),
258 'auto_close' => true,
259 )
260 );
261 }
262
263 /**
264 * Provides data for the frontend.
265 *
266 * @return array An array of data for the frontend.
267 */
268 public function data_frontend(): array {
269 $model = $this->get_model();
270
271 $this->service->maybe_create_default_config();
272 $configs = $this->get_configs_and_update_status();
273
274 foreach ( $configs as &$config ) {
275 // Unset the data as we don't need it.
276 if ( isset( $config['configs'] ) ) {
277 unset( $config['configs'] );
278 }
279 }
280
281 $link = $this->wpmudev->is_member()
282 ? 'https://wpmudev.com/translate/projects/wpdef/'
283 : 'https://translate.wordpress.org/projects/wp-plugins/defender-security/';
284 $can_whitelabel = wd_di()->get( \WP_Defender\Integrations\Dashboard_Whitelabel::class )->can_whitelabel();
285
286 return array_merge(
287 array(
288 'general' => array(
289 'translate' => $model->translate,
290 'show_usage_tracking' => $this->wpmudev->is_wpmu_dev_admin() || ! $can_whitelabel,
291 'usage_tracking' => $model->usage_tracking,
292 'translation_link' => $link,
293 ),
294 'data_settings' => array(
295 'uninstall_settings' => $model->uninstall_settings,
296 'uninstall_data' => $model->uninstall_data,
297 'uninstall_quarantine' => $model->uninstall_quarantine,
298 ),
299 'accessibility' => array(
300 'high_contrast_mode' => $model->high_contrast_mode,
301 ),
302 'misc' => array(
303 'setting_url' => network_admin_url( is_multisite() ? 'settings.php' : 'options-general.php' ),
304 'privacy_link' => Model_Main_Setting::PRIVACY_LINK,
305 ),
306 'configs' => $configs,
307 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
308 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
309 ),
310 $this->dump_routes_and_nonces()
311 );
312 }
313
314 /**
315 * Converts the current object state to an array.
316 *
317 * @return array The array representation of the object.
318 */
319 public function to_array(): array {
320 return array();
321 }
322
323 /**
324 * Imports data into the model.
325 *
326 * @param array $data Data to be imported into the model.
327 */
328 public function import_data( array $data ) {
329 $model = $this->get_model();
330
331 $model->import( $data );
332 if ( $model->validate() ) {
333 $model->save();
334 }
335 }
336
337 /**
338 * Validates the importer data.
339 * This function checks if the importer data is valid by verifying its configuration data and comparing it with the
340 * sample data.
341 *
342 * @param array $importer The importer data to be validated.
343 *
344 * @return bool Returns true if the importer data is valid, false otherwise.
345 */
346 private function validate_importer( $importer ): bool {
347 if ( $this->service->verify_config_data( $importer ) ) {
348 // Validate content. This is the current data, we use this for verify the schema.
349 $sample = $this->service->gather_data();
350 foreach ( $importer['configs'] as $slug => $module ) {
351 // This is not in the sample, file is invalid.
352 if ( ! isset( $sample[ $slug ] ) ) {
353 return false;
354 }
355
356 $keys = array_keys( $sample[ $slug ] );
357 $import_keys = array_keys( $module );
358 $diff = array_diff( $import_keys, $keys );
359 if ( count( $diff ) ) {
360 return false;
361 }
362
363 return true;
364 }
365 }
366
367 return false;
368 }
369
370 /**
371 * Import config.
372 *
373 * @return Response
374 * @defender_route
375 */
376 public function import_config(): Response {
377 global $wp_filesystem;
378 // Initialize the WP filesystem, no more using 'file-put-contents' function.
379 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
380 require_once ABSPATH . '/wp-admin/includes/file.php';
381 WP_Filesystem();
382 }
383 $file = defender_get_data_from_request( 'file', 'f' );
384 $tmp = $file['tmp_name'];
385 $content = $wp_filesystem->get_contents( $tmp );
386 $importer = json_decode( $content, true );
387 if ( ! is_array( $importer ) ) {
388 return new Response(
389 false,
390 array(
391 'message' => esc_html__( 'The file is corrupted.', 'defender-security' ),
392 )
393 );
394 }
395
396 // If it's old config structure then we upgrade configs to new format.
397 if (
398 isset( $importer['configs'] ) &&
399 is_array( $importer['configs'] ) &&
400 array() !== $importer['configs'] &&
401 ! $this->service->check_for_new_structure( $importer['configs'] )
402 ) {
403 $adapter = wd_di()->get( Config_Adapter::class );
404 $importer['configs'] = $adapter->upgrade( $importer['configs'] );
405 }
406
407 if ( ! $this->validate_importer( $importer ) ) {
408 return new Response(
409 false,
410 array(
411 'message' => esc_html__(
412 'An error occurred while importing the file. Please check your file or upload another file.',
413 'defender-security'
414 ),
415 )
416 );
417 }
418
419 // Do not use wp_strip_all_tags() to prevent XSS attack.
420 $name = sanitize_text_field( $importer['name'] );
421 $configs = array(
422 'name' => $name,
423 'immortal' => false,
424 'is_removable' => true,
425 );
426
427 $configs['configs'] = $importer['configs'];
428 $configs['description'] = isset( $importer['description'] ) && is_string( $importer['description'] ) && '' !== $importer['description']
429 ? sanitize_textarea_field( $importer['description'] )
430 : '';
431 $configs['strings'] = $this->service->import_module_strings( $importer );
432 $key = 'wp_defender_config_import_' . time();
433 update_site_option( $key, $configs );
434 $this->service->index_key( $key );
435
436 return new Response(
437 true,
438 array(
439 'message' => sprintf(
440 /* translators: %s: Config name. */
441 esc_html__(
442 '%s config has been uploaded successfully – you can now apply it to this site.',
443 'defender-security'
444 ),
445 '<strong>' . $name . '</strong>'
446 ),
447 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
448 )
449 );
450 }
451
452 /**
453 * Create config.
454 *
455 * @param Request $request The request object containing new config data.
456 *
457 * @return Response
458 * @defender_route
459 */
460 public function new_config( Request $request ): Response {
461 $data = $request->get_data();
462 $name = trim( $data['name'] );
463 if ( '' === $name ) {
464 return new Response(
465 false,
466 array(
467 'message' => esc_html__( 'Invalid config name', 'defender-security' ),
468 )
469 );
470 }
471 $name = sanitize_text_field( $name );
472 $desc = isset( $data['desc'] ) && is_string( $data['desc'] ) && '' !== trim( $data['desc'] ) ? wp_kses_post( $data['desc'] ) : '';
473 $key = 'wp_defender_config_' . time();
474 $settings = $this->service->parse_data_for_import();
475 $data = array_merge(
476 array(
477 'name' => $name,
478 'immortal' => false,
479 'description' => $desc,
480 'is_removable' => true,
481 ),
482 $settings
483 );
484
485 // Add config to HUB.
486 $hub_id = Config_Hub_Helper::add_configs_to_hub( $data );
487
488 if ( $hub_id ) {
489 $data['hub_id'] = $hub_id;
490 }
491
492 unset( $data['labels'] );
493
494 if ( update_site_option( $key, $data ) ) {
495 $this->service->index_key( $key );
496
497 return new Response(
498 true,
499 array(
500 'message' => sprintf(
501 /* translators: %s: Config name. */
502 esc_html__( '%s config saved successfully.', 'defender-security' ),
503 '<strong>' . $name . '</strong>'
504 ),
505 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
506 )
507 );
508 } else {
509 return new Response(
510 false,
511 array(
512 'message' => esc_html__(
513 'An error occurred while saving your config. Please try it again.',
514 'defender-security'
515 ),
516 )
517 );
518 }
519 }
520
521 /**
522 * Download config
523 *
524 * @return Response|void
525 * @defender_route
526 */
527 public function download_config() {
528 $key = defender_get_data_from_request( 'key', 'g' );
529 if ( ! is_string( $key ) || '' === trim( $key ) ) {
530 return new Response(
531 false,
532 array(
533 'message' => esc_html__( 'Invalid config', 'defender-security' ),
534 )
535 );
536 }
537
538 $config = get_site_option( $key );
539 if ( false === $config ) {
540 return new Response(
541 false,
542 array(
543 'message' => esc_html__( 'Invalid config', 'defender-security' ),
544 )
545 );
546 }
547 $sample = $this->service->gather_data();
548 foreach ( $sample as $slug => $data ) {
549 foreach ( $data as $k => $val ) {
550 if ( ! isset( $config['configs'][ $slug ][ $k ] ) ) {
551 $config['configs'][ $slug ][ $k ] = null;
552 }
553 }
554 }
555 $filename = 'wp-defender-config-' . sanitize_file_name( $config['name'] ) . '.json';
556 header( 'Content-disposition: attachment; filename=' . $filename );
557 echo wp_json_encode( $config, JSON_PRETTY_PRINT );
558 exit();
559 }
560
561 /**
562 * Apply config.
563 *
564 * @param Request $request The request object containing new config data.
565 *
566 * @return Response
567 * @defender_route
568 */
569 public function apply_config( Request $request ) {
570 $data = $request->get_data();
571 $key = trim( $data['key'] );
572 if ( '' === $key ) {
573 return new Response(
574 false,
575 array(
576 'message' => esc_html__( 'Invalid config', 'defender-security' ),
577 )
578 );
579 }
580
581 $config = get_site_option( $key );
582 if ( false === $config ) {
583 return new Response(
584 false,
585 array(
586 'message' => esc_html__( 'Invalid config', 'defender-security' ),
587 )
588 );
589 }
590 // Return error message or bool value for auth action.
591 $restore_result = $this->service->restore_data( $config['configs'], 'plugin' );
592 if ( is_string( $restore_result ) ) {
593 return $this->apply_config_recommendations_error_message();
594 }
595
596 $this->service->make_config_active( $key );
597 // Track.
598 $this->track_feature(
599 'def_config_applied',
600 array(
601 // The check is based on the fact that the Default config cannot be deleted.
602 'Config Type' => isset( $config['is_removable'] ) && false === $config['is_removable'] ? 'Default' : 'Custom',
603 )
604 );
605
606 $message = sprintf(
607 /* translators: %s: Config name. */
608 esc_html__(
609 '%s config has been applied successfully.',
610 'defender-security'
611 ),
612 '<strong>' . $config['name'] . '</strong>'
613 );
614 $return = array();
615 if ( $restore_result ) {
616 $login_url = wp_login_url();
617 $settings_mask_login = new \WP_Defender\Model\Setting\Mask_Login();
618 if ( $settings_mask_login->is_active() ) {
619 $login_url = $settings_mask_login->get_new_login_url();
620 }
621 $message .= '<br/>' . sprintf(
622 /* translators: %s: Login link. */
623 esc_html__(
624 'Due to currently applied security recommendations, you will now need to %s.',
625 'defender-security'
626 ),
627 '<a href="' . $login_url . '"><strong>' . esc_html__( 're-login', 'defender-security' ) . '</strong></a>'
628 );
629 $message .= '<br/>';
630 $message .= esc_html__( 'This will auto reload now.', 'defender-security' );
631
632 $return['reload'] = 3;
633 $redirect = rawurlencode( network_admin_url( 'admin.php?page=wdf-setting&view=configs' ) );
634 if ( isset( $data['screen'] ) && 'dashboard' === $data['screen'] ) {
635 $redirect = rawurlencode( network_admin_url( 'admin.php?page=wp-defender' ) );
636 }
637 $return['redirect'] = add_query_arg(
638 'redirect_to',
639 $redirect,
640 $login_url
641 );
642 $return['interval'] = 2;
643 }
644
645 $return['message'] = $message;
646 $return['auto_close'] = true;
647 $return['configs'] = Config_Hub_Helper::get_fresh_frontend_configs( $this->service );
648
649 return new Response( true, $return );
650 }
651
652 /**
653 * Update config.
654 *
655 * @param Request $request The request object containing new config data.
656 *
657 * @return Response
658 * @defender_route
659 */
660 public function update_config( Request $request ) {
661 $data = $request->get_data();
662 $key = trim( $data['key'] );
663 $name = trim( $data['name'] );
664 $description = trim( $data['desc'] );
665 if ( '' === $name || '' === $key ) {
666 return new Response(
667 false,
668 array(
669 'message' => esc_html__( 'Invalid config', 'defender-security' ),
670 )
671 );
672 }
673
674 $config = get_site_option( $key );
675 if ( false === $config ) {
676 return new Response(
677 false,
678 array(
679 'message' => esc_html__( 'Invalid config', 'defender-security' ),
680 )
681 );
682 }
683
684 $old_config = $config;
685 $config['name'] = sanitize_text_field( $name );
686 $config['description'] = sanitize_textarea_field( $description );
687
688 // Check data has been changed or not.
689 if (
690 $old_config['name'] === $config['name'] &&
691 $old_config['description'] === $config['description']
692 ) {
693 // Data is not changed, so not need to run update query.
694 $option_updated = true;
695 } else {
696 $option_updated = update_site_option( $key, $config );
697 Config_Hub_Helper::update_on_hub( $config );
698 }
699
700 if ( $option_updated ) {
701 return new Response(
702 true,
703 array(
704 'message' => sprintf(
705 /* translators: %s: Config name. */
706 esc_html__( '%s config saved successfully.', 'defender-security' ),
707 '<strong>' . $name . '</strong>'
708 ),
709 'auto_close' => true,
710 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
711 )
712 );
713 } else {
714 return new Response(
715 false,
716 array(
717 'message' => esc_html__(
718 'An error occurred while saving your config. Please try it again.',
719 'defender-security'
720 ),
721 )
722 );
723 }
724 }
725
726 /**
727 * Delete config.
728 *
729 * @param Request $request The request object containing config key.
730 *
731 * @return Response
732 * @defender_route
733 */
734 public function delete_config( Request $request ) {
735 $data = $request->get_data();
736 $key = trim( $data['key'] );
737 if ( '' === $key ) {
738 return new Response(
739 false,
740 array(
741 'message' => esc_html__( 'Invalid config', 'defender-security' ),
742 )
743 );
744 }
745
746 $config = get_site_option( $key );
747 if ( isset( $config['is_removable'] ) && false === (bool) $config['is_removable'] ) {
748 return new Response(
749 false,
750 array(
751 'message' => esc_html__( 'Config can\'t be removed', 'defender-security' ),
752 )
753 );
754 }
755
756 // Remove from HUB.
757 if ( isset( $config['hub_id'] ) ) {
758 Config_Hub_Helper::delete_configs_from_hub( (int) $config['hub_id'] );
759 }
760
761 if ( 0 === strpos( $key, 'wp_defender_config' ) ) {
762 delete_site_option( $key );
763 $this->service->clear_keys();
764
765 return new Response(
766 true,
767 array(
768 'message' => esc_html__( 'Config removed successfully.', 'defender-security' ),
769 'auto_close' => true,
770 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
771 )
772 );
773 }
774
775 return new Response(
776 false,
777 array(
778 'message' => esc_html__( 'Invalid config', 'defender-security' ),
779 )
780 );
781 }
782
783 /**
784 * Exports strings.
785 *
786 * @return array An array of strings.
787 */
788 public function export_strings(): array {
789 return array();
790 }
791
792 /**
793 * Update config status and return them.
794 *
795 * @return array
796 */
797 private function get_configs_and_update_status() {
798 $configs = Config_Hub_Helper::get_configs( $this->service );
799 $is_remove = Config_Hub_Helper::check_remove_active_flag();
800
801 // Loop to update strings of configs.
802 foreach ( $configs as $key => &$config ) {
803 if ( ! is_array( $config ) ) {
804 continue;
805 }
806
807 $config['strings'] = $this->service->import_module_strings( $config );
808
809 if ( $is_remove ) {
810 $config['is_active'] = false;
811 }
812
813 // Update config data.
814 update_site_option( $key, $config );
815 }
816
817 return $configs;
818 }
819
820 /**
821 * Generates an error message for when there is an issue applying some tweaks from the Recommendations tab.
822 *
823 * @return Response The response object containing the error message and fresh frontend configurations.
824 */
825 private function apply_config_recommendations_error_message(): Response {
826 $message = sprintf(
827 /* translators: 1: Recommendations tab, 2: wp-config.php file, 3: Documentation. */
828 esc_html__(
829 '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.',
830 'defender-security'
831 ),
832 '<strong>' . esc_html__( 'Recommendations', 'defender-security' ) . '</strong>',
833 '<strong>' . esc_html__( 'wp-config.php', 'defender-security' ) . '</strong>',
834 '<a href="' . WP_DEFENDER_DOCS_LINK . '#manually-applying-recommendations" target="_blank">' . esc_html__( 'documentation', 'defender-security' ) . '</a>'
835 );
836
837 return new Response(
838 false,
839 array(
840 'message' => $message,
841 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ),
842 )
843 );
844 }
845
846 /**
847 * Clear out lines that are older than 30 days.
848 *
849 * @return void
850 */
851 public function clear_logs(): void {
852 // since 2.7.0.
853 $time_limit = apply_filters( 'wpdef_clear_logs_time_limit', MONTH_IN_SECONDS );
854
855 if ( is_multisite() ) {
856 global $wpdb;
857 $offset = 0;
858 $limit = 100;
859 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
860 $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
861 ARRAY_A
862 );
863 while ( is_array( $blogs ) && array() !== $blogs ) {
864 foreach ( $blogs as $blog ) {
865 switch_to_blog( $blog['blog_id'] );
866
867 $this->clear_logs_from_files( $time_limit );
868
869 restore_current_blog();
870 }
871 $offset += $limit;
872 $blogs = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
873 $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ),
874 ARRAY_A
875 );
876 }
877 } else {
878 $this->clear_logs_from_files( $time_limit );
879 }
880 }
881
882 /**
883 * Clear log files older than the specified time.
884 *
885 * @param int $time_limit The time limit in seconds.
886 *
887 * @return void
888 * @since 2.7.0
889 */
890 public function clear_logs_from_files( int $time_limit = MONTH_IN_SECONDS ) {
891 global $wp_filesystem;
892 // Initialize the WP filesystem, no more using 'file-put-contents' function.
893 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
894 require_once ABSPATH . '/wp-admin/includes/file.php';
895 WP_Filesystem();
896 }
897 $now = wp_date( 'c' );
898 $files = array( wd_internal_log() );
899
900 foreach ( $files as $file_name ) {
901 $file_path = $this->get_log_path( $file_name );
902
903 if ( ! file_exists( $file_path ) ) {
904 return;
905 }
906
907 $content = file( $file_path );
908 $size_of_content = is_array( $content ) || $content instanceof Countable ? count( $content ) : 0;
909
910 foreach ( $content as $index => $line ) {
911 // If the line does not start with '[' (it's probably not a new entry).
912 $first_char = substr( $line, 0, 1 );
913
914 if ( '[' !== $first_char ) {
915 // Delete.
916 unset( $content[ $index ] );
917 }
918
919 /**
920 * Get the date from entry. Items can be an array it two cases - if there's a valid date, or if the line
921 * contained something like [header] in the start. Cannot make assumptions just on the fact it's an array.
922 */
923 preg_match( '/\[(.*)\]/', $line, $items );
924
925 // If, for some reason, can't get the date, or it's not the size of an ISO 8601 date.
926 if ( ! isset( $items[1] ) || 25 !== strlen( $items[1] ) ) {
927 // Delete.
928 unset( $content[ $index ] );
929 } else {
930 // It looks like it's a valid date string, compare with today.
931 $time_diff = strtotime( $now ) - strtotime( $items[1] );
932
933 // We don't need to continue on, because if this entry is not older than specific time, the next one will not be as well.
934 if ( $time_diff < $time_limit ) {
935 break;
936 }
937
938 unset( $content[ $index ] );
939 }
940 }
941
942 // Nothing changed - do nothing.
943 if ( ( is_array( $content ) || $content instanceof Countable ? count( $content ) : 0 ) === $size_of_content ) {
944 return;
945 }
946
947 // Glue back together and write back to file.
948 $content = implode( '', $content );
949
950 $wp_filesystem->put_contents( $file_path, $content );
951 }
952 }
953
954 /**
955 * Track the data if there are settings changes.
956 *
957 * @param array $old_settings Old settings.
958 * @param array $new_settings New settings.
959 *
960 * @return void
961 * @since 4.2.0
962 */
963 public function intercept_settings_update( $old_settings, $new_settings ) {
964 $from = $this->get_triggered_location();
965 if (
966 '' !== $from
967 && isset( $new_settings['usage_tracking'], $old_settings['usage_tracking'] )
968 && $new_settings['usage_tracking'] !== $old_settings['usage_tracking']
969 ) {
970 $this->track_opt_toggle( (bool) $new_settings['usage_tracking'], $from );
971 }
972 }
973 }
974