PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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-scan.php

class-scan.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/controller/class-scan.php

1,064 lines 28.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles all scan related actions.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use ActionScheduler;
11 use WP_Defender\Event;
12 use Valitron\Validator;
13 use Calotes\Component\Request;
14 use Calotes\Component\Response;
15 use WP_Defender\Controller\Quarantine;
16 use WP_Defender\Traits\Formats;
17 use WP_Defender\Traits\Scan_Upsell;
18 use WP_Defender\Model\Scan_Item;
19 use WP_Defender\Behavior\WPMUDEV;
20 use WP_Defender\Model\Scan as Model_Scan;
21 use WP_Defender\Behavior\Scan\Core_Integrity;
22 use WP_Defender\Component\Network_Cron_Manager;
23 use WP_Defender\Component\Scan as Scan_Component;
24 use WP_Defender\Model\Setting\Scan as Scan_Settings;
25 use WP_Defender\Model\Notification\Malware_Report;
26 use WP_Defender\Component\Config\Config_Hub_Helper;
27 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
28 use WP_Defender\Model\Notification\Malware_Notification;
29 use WP_Defender\Component\Quarantine as Quarantine_Component;
30 use WP_Defender\Behavior\Scan\Plugin_Integrity;
31
32 /**
33 * Contains methods for handling scans.
34 */
35 class Scan extends Event {
36
37 use Formats;
38 use Scan_Upsell;
39
40 public const SCAN_LOG = 'scan.log';
41 /**
42 * The slug identifier for this controller.
43 *
44 * @var string
45 */
46 protected $slug = 'wdf-scan';
47
48 /**
49 * The model for handling the data.
50 *
51 * @var Scan_Settings
52 */
53 protected $model;
54
55 /**
56 * Service for handling logic.
57 *
58 * @var Scan_Component
59 */
60 protected $service;
61
62 /**
63 * Quarantine controller.
64 *
65 * @var Quarantine
66 */
67 private $quarantine_controller;
68
69 /**
70 * Indicates whether the current installation is a pro version.
71 *
72 * @var bool
73 */
74 private $is_pro;
75
76 /**
77 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
78 */
79 public function __construct() {
80 $this->register_page(
81 $this->get_title(),
82 $this->slug,
83 array( $this, 'main_view' ),
84 $this->parent_slug
85 );
86
87 $this->model = new Scan_Settings();
88 $this->service = wd_di()->get( Scan_Component::class );
89 $this->is_pro = wd_di()->get( WPMUDEV::class )->is_pro();
90
91 if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
92 $this->quarantine_controller = wd_di()->get( Quarantine::class );
93 }
94
95 $this->register_routes();
96 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
97 add_action( 'wp_ajax_defender_process_scan', array( $this, 'process' ) );
98 add_action( 'wp_ajax_nopriv_defender_process_scan', array( $this, 'process' ) );
99 add_action( 'defender/async_scan', array( $this, 'process' ) );
100 // Clean up data after successful core update.
101 add_action( '_core_updated_successfully', array( $this, 'clean_up_data' ) );
102
103 global $pagenow;
104 // since 2.6.2.
105 if (
106 is_admin() &&
107 'plugins.php' === $pagenow &&
108 apply_filters( 'wd_display_vulnerability_warnings', true ) &&
109 $this->is_pro
110 ) {
111 $this->service->display_vulnerability_warnings();
112 }
113
114 /**
115 * Schedule a time to clear completed action scheduler logs.
116 *
117 * @var Network_Cron_Manager $network_cron_manager
118 */
119 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
120 $network_cron_manager->register_callback(
121 'wpdef_clear_scan_logs',
122 array( $this, 'clear_scan_logs' ),
123 WEEK_IN_SECONDS
124 );
125
126 add_filter( 'heartbeat_nopriv_send', array( $this, 'nopriv_heartbeat' ), 10, 2 );
127
128 add_action(
129 'action_scheduler_completed_action',
130 array( $this, 'scan_completed_analytics' )
131 );
132 }
133
134 /**
135 * Return the title of the page.
136 *
137 * @return string The title of the page.
138 */
139 public function get_title(): string {
140 return esc_html__( 'Malware Scanning', 'defender-security' );
141 }
142
143 /**
144 * Clean up data after core updating.
145 *
146 * @return void
147 */
148 public function clean_up_data(): void {
149 $this->service->clean_up();
150 }
151
152 /**
153 * Start a scan.
154 *
155 * @return Response
156 * @defender_route
157 * @defender_redirect
158 */
159 public function start(): Response {
160 $model = Model_Scan::create();
161 if ( is_object( $model ) && ! is_wp_error( $model ) ) {
162 $this->log( 'Initial ping self', self::SCAN_LOG );
163 $this->service->gather_actioned_plugin_details();
164
165 $this->do_async_scan( 'scan' );
166
167 return new Response(
168 true,
169 array(
170 'status' => $model->status,
171 'status_text' => $model->get_status_text(),
172 'percent' => 0,
173 )
174 );
175 }
176
177 return new Response(
178 false,
179 array(
180 'message' => esc_html__( 'A scan is already in progress', 'defender-security' ),
181 )
182 );
183 }
184
185 /**
186 * Use this for self ping, so it can both run in background and active mode with good performance.
187 *
188 * @return void
189 * @defender_route
190 * @is_public
191 */
192 public function process() {
193 if ( $this->service->has_lock() ) {
194 $this->log( 'Fallback as already a process is running', self::SCAN_LOG );
195
196 return;
197 }
198
199 // This creates file lock, for make sure only 1 process run as a time.
200 $this->service->create_lock();
201 // Check if the ping is from self or not.
202 $ret = $this->service->process();
203 $this->log( 'process done, queue for next', self::SCAN_LOG );
204 if ( false === $ret ) {
205 // Ping self.
206 $this->log( 'Scan not done, pinging', self::SCAN_LOG );
207 $this->service->remove_lock();
208 $this->process();
209 } else {
210 $this->queue_to_sync_with_hub();
211 $this->service->remove_lock();
212 }
213 }
214
215 /**
216 * Query status.
217 *
218 * @return Response
219 * @defender_route
220 * @defender_redirect
221 */
222 public function status(): Response {
223 $idle_scan = wd_di()->get( Model_Scan::class )->get_idle();
224
225 if ( is_object( $idle_scan ) ) {
226 $this->service->update_idle_scan_status();
227
228 return new Response( false, $idle_scan->to_array() );
229 }
230
231 $checksum_issue = get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 'false' );
232 $checksum_scan = Model_Scan::get_core_check();
233 if ( 'false' !== $checksum_issue && is_object( $checksum_scan ) ) {
234 $this->service->update_idle_scan_status_by_checksum_issue( $checksum_scan );
235
236 return new Response( false, $checksum_scan->to_array() );
237 }
238
239 $scan = Model_Scan::get_active();
240 if ( is_object( $scan ) ) {
241
242 return new Response( false, $scan->to_array() );
243 }
244 $scan = Model_Scan::get_last();
245 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
246
247 return new Response( true, $scan->to_array() );
248 }
249
250 return new Response(
251 false,
252 array(
253 'message' => esc_html__( 'Error during scanning', 'defender-security' ),
254 )
255 );
256 }
257
258 /**
259 * Cancel current scan.
260 *
261 * @return Response
262 * @defender_route
263 * @defender_redirect
264 */
265 public function cancel(): Response {
266 $component = wd_di()->get( Scan_Component::class );
267 $component->cancel_a_scan();
268 $last = Model_Scan::get_last();
269 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
270 $last = $last->to_array();
271 }
272
273 return new Response(
274 true,
275 array(
276 'scan' => $last,
277 )
278 );
279 }
280
281 /**
282 * Track scan item action analytics.
283 *
284 * @param Scan_Item $scan_item Individual item of scan issues list.
285 * @param string $intention What action is going to be executed.
286 */
287 private function item_action_analytics( Scan_Item $scan_item, string $intention ) {
288 $allowed_intentions = Scan_Component::get_intentions();
289
290 $event_name = 'def_threat_resolved';
291
292 if ( in_array( $intention, $allowed_intentions, true ) ) {
293 $intention_desc = array(
294 'resolve' => 'Safe Repair',
295 'ignore' => 'Ignore',
296 'delete' => 'Delete',
297 'unignore' => 'Unignore',
298 'quarantine' => 'Safe Repair & Quarantine',
299 );
300
301 $resolution_method = $intention_desc[ $intention ];
302 $threat_type = '';
303
304 if ( Scan_Item::TYPE_INTEGRITY === $scan_item->type ) {
305 // Track Repair-actions.
306 if ( in_array( $intention, array( 'resolve', 'quarantine' ), true ) ) {
307 $threat_type = 'core file modified';
308 } else {
309 $threat_type = 'Unknown file in WordPress core';
310 }
311 } elseif ( Scan_Item::TYPE_PLUGIN_CHECK === $scan_item->type ) {
312 $raw_data = $scan_item->raw_data;
313
314 if ( isset( $raw_data['type'] ) && 'modified' === $raw_data['type'] ) {
315 $threat_type = 'plugin file modified';
316 }
317 } elseif ( Scan_Item::TYPE_VULNERABILITY === $scan_item->type ) {
318 $threat_type = 'Vulnerability';
319
320 if ( 'resolve' === $intention ) {
321 $resolution_method = 'Update';
322 }
323 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $scan_item->type ) {
324 $threat_type = 'Suspicious function';
325 } elseif (
326 in_array(
327 $scan_item->type,
328 Model_Scan::get_abandoned_types(),
329 true
330 )
331 ) {
332 $threat_type = 'Outdated & removed plugins';
333 }
334
335 $this->track_feature(
336 $event_name,
337 array(
338 'Resolution Method' => $resolution_method,
339 'Threat type' => $threat_type,
340 )
341 );
342 }
343 }
344
345 /**
346 * A central controller to pass any request from frontend to scan item.
347 *
348 * @param Request $request Request object.
349 *
350 * @return Response
351 * @defender_route
352 */
353 public function item_action( Request $request ): Response {
354 $data = $request->get_data(
355 array(
356 'id' => array(
357 'type' => 'int',
358 'sanitize' => 'sanitize_text_field',
359 ),
360 'intention' => array(
361 'type' => 'string',
362 'sanitize' => 'sanitize_text_field',
363 ),
364 'parent_action' => array(
365 'type' => 'string',
366 'sanitize' => 'sanitize_text_field',
367 ),
368 )
369 );
370 $id = $data['id'] ?? false;
371 $intention = $data['intention'] ?? false;
372 // Get allowed intentions.
373 $allowed_intentions = Scan_Component::get_intentions();
374 $allowed_intentions[] = 'pull_src';
375 if ( false === $id || false === $intention || ! in_array(
376 $intention,
377 $allowed_intentions,
378 true
379 ) ) {
380 wp_die();
381 }
382
383 $scan = Model_Scan::get_last();
384 if ( $scan instanceof Model_Scan ) {
385 $item = $scan->get_issue( $id );
386 if ( is_object( $item ) && $item->has_method( $intention ) ) {
387
388 if ( 'quarantine' === $intention ) {
389 $result = $item->$intention( $data['parent_action'] );
390 } else {
391 $result = $item->$intention();
392 }
393 // Maybe track.
394 if ( $this->is_tracking_active() ) {
395 $this->item_action_analytics( $item, $intention );
396 }
397
398 if ( is_wp_error( $result ) ) {
399 return new Response(
400 false,
401 array(
402 'message' => $result->get_error_message(),
403 )
404 );
405 } elseif ( isset( $result['type_notice'] ) ) {
406 return new Response(
407 true,
408 $result
409 );
410 } elseif ( isset( $result['url'] ) ) {
411 // Without message and interval args.
412 return new Response(
413 true,
414 array( 'redirect' => $result['url'] )
415 );
416 }
417
418 $this->queue_to_sync_with_hub();
419
420 // Refresh scan instance.
421 $scan = Model_Scan::get_last();
422
423 if ( $scan instanceof Model_Scan ) {
424 $result['scan'] = $scan->to_array();
425
426 $success = true;
427 if ( isset( $result['success'] ) && false === $result['success'] ) {
428 $success = false;
429 }
430
431 return new Response( $success, $result );
432 }
433 }
434 }
435
436 return new Response( false, array() );
437 }
438
439 /**
440 * Process for bulk action.
441 * There is no Update-intention because it is a lengthy process. There may not be enough execution time.
442 *
443 * @param Request $request Request object.
444 *
445 * @defender_route
446 * @return Response
447 */
448 public function bulk_action( Request $request ): Response {
449 $data = $request->get_data(
450 array(
451 'items' => array(
452 'type' => 'array',
453 'sanitize' => 'sanitize_text_field',
454 ),
455 'bulk' => array(
456 'type' => 'string',
457 'sanitize' => 'sanitize_text_field',
458 ),
459 )
460 );
461 $items = $data['items'] ?? array();
462 $intention = $data['bulk'] ?? false;
463
464 if (
465 empty( $items )
466 || ! is_array( $items )
467 || ! in_array( $intention, array( 'ignore', 'unignore', 'delete' ), true )
468 ) {
469 return new Response( false, array() );
470 }
471 // Try to get Scan.
472 $scan = Model_Scan::get_last();
473 if ( ! is_object( $scan ) ) {
474 return new Response( false, array() );
475 }
476
477 $is_delete = false;
478 $delete_items = array();
479 $none_delete_items = array();
480 $sync_hub = false;
481 foreach ( $items as $id ) {
482 if ( 'ignore' === $intention ) {
483 $scan->ignore_issue( (int) $id );
484 $sync_hub = true;
485 } elseif ( 'unignore' === $intention ) {
486 $scan->unignore_issue( (int) $id );
487 $sync_hub = true;
488 } elseif ( 'delete' === $intention ) {
489 $item = $scan->get_issue( (int) $id );
490 // Work with every item.
491 if ( is_object( $item ) && $item->has_method( $intention ) ) {
492 $item_result = $item->delete();
493 if ( is_wp_error( $item_result ) ) {
494 $none_delete_items[] = $item_result->get_error_message();
495 } elseif ( isset( $item_result['type_notice'] ) ) {
496 return new Response( true, $item_result );
497 } elseif ( isset( $item_result['collect_type'] ) ) {
498 $is_delete = true;
499 $delete_items[] = $item_result['message'];
500 }
501 // If there is any error, no need to sync data.
502 $sync_hub = true;
503 }
504 }
505 }
506
507 if ( $sync_hub ) {
508 $this->queue_to_sync_with_hub();
509 }
510
511 $result = array();
512 if ( ! empty( $none_delete_items ) ) {
513 $result['message'] = sprintf(
514 /* translators: %s: Vulnerability item(es) */
515 _n(
516 'Defender doesn\'t have enough permission to remove this file: %s',
517 'Defender doesn\'t have enough permission to remove these files: %s',
518 count( $none_delete_items ),
519 'defender-security'
520 ),
521 '<pre>' . implode( PHP_EOL, $none_delete_items ) . '</pre>'
522 );
523 } elseif ( $is_delete ) {
524 $result['message'] = sprintf(
525 /* translators: %s: Vulnerability item(es) */
526 esc_html__( '%s has (have) been deleted', 'defender-security' ),
527 implode( ', ', $delete_items )
528 );
529 }
530 // Refresh scan instance.
531 $scan = Model_Scan::get_last();
532 $result['scan'] = $scan->to_array();
533
534 return new Response( empty( $none_delete_items ), $result );
535 }
536
537 /**
538 * Save settings.
539 *
540 * @param Request $request The request object containing new settings data.
541 *
542 * @return Response
543 * @since 2.7.0 Add Scheduled Scanning to Malware settings and hide it on Malware Scanning - Reporting.
544 * Also, the backward compatibility of settings for Scan and Malware_Report models.
545 * @defender_route
546 */
547 public function save_settings( Request $request ): Response {
548 $data = $request->get_data_by_model( $this->model );
549 // Case#1: enable all child options, if parent and all child options are disabled, so that there is no notice when saving.
550 if (
551 ! $data['integrity_check']
552 && ! $data['check_core']
553 && ! $data['check_plugins']
554 ) {
555 $data['check_core'] = true;
556 $data['check_plugins'] = true;
557 }
558
559 // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice.
560 if ( $data['scan_malware'] && ! $data['integrity_check'] ) {
561 $response = array(
562 'type_notice' => 'info',
563 'message' => sprintf(
564 /* translators: 1. Open tag. 2. Close tag. 3. Open tag. 4. Close tag. */
565 esc_html__(
566 'To reduce false-positive results, we recommend enabling %1$sFile change detection%2$s options for all scan types while the %3$sSuspicious code%4$s option is enabled.',
567 'defender-security'
568 ),
569 '<strong>',
570 '</strong>',
571 '<strong>',
572 '</strong>'
573 ),
574 );
575 } else {
576 // Prepare response message for usual successful case.
577 $response = array(
578 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
579 'auto_close' => true,
580 );
581 }
582 // Additional cases are in the Scan model.
583 $report_change = false;
584 // If 'Scheduled Scanning' is checked then need to change Malware_Report.
585 if ( true === $data['scheduled_scanning'] ) {
586 $report = new Malware_Report();
587 $report_change = true;
588 $report->frequency = $data['frequency'];
589 $report->day = $data['day'];
590 $report->day_n = (int) $data['day_n'];
591 $report->time = $data['time'];
592 // Disable 'Scheduled Scanning'.
593 } elseif ( true === $this->model->scheduled_scanning && false === $data['scheduled_scanning'] ) {
594 $report = new Malware_Report();
595 $report_change = true;
596 $report->status = \WP_Defender\Model\Notification::STATUS_DISABLED;
597 }
598
599 $before_import_schedule = $this->model->quarantine_expire_schedule;
600
601 $this->model->import( $data );
602 if ( $this->model->validate() ) {
603
604 if ( class_exists( 'WP_Defender\Component\Quarantine' ) ) {
605 $quarantine_component = wd_di()->get( Quarantine_Component::class );
606 $quarantine_component->reschedule_file_expiry_cron(
607 $before_import_schedule,
608 $data['quarantine_expire_schedule']
609 );
610 }
611
612 // Todo: need to disable Malware_Notification & Malware_Report if all scan settings are deactivated?
613 $this->model->save();
614 // Save Report's changes.
615 if ( $report_change ) {
616 $report->save();
617 }
618 Config_Hub_Helper::set_clear_active_flag();
619
620 return new Response(
621 true,
622 array_merge( $response, $this->data_frontend() )
623 );
624 } else {
625 return new Response(
626 false,
627 array_merge(
628 array(
629 'message' => $this->model->get_formatted_errors(),
630 ),
631 $this->data_frontend()
632 )
633 );
634 }
635 }
636
637 /**
638 * Get the issues mainly for pagination request.
639 *
640 * @param Request $request The request object.
641 *
642 * @return Response
643 * @defender_route
644 */
645 public function get_issues( Request $request ): Response {
646 $data = $request->get_data(
647 array(
648 'scenario' => array(
649 'type' => 'string',
650 'sanitize' => 'sanitize_text_field',
651 ),
652 'type' => array(
653 'type' => 'string',
654 'sanitize' => 'sanitize_text_field',
655 ),
656 'per_page' => array(
657 'type' => 'string',
658 'sanitize' => 'intval',
659 ),
660 'paged' => array(
661 'type' => 'int',
662 'sanitize' => 'intval',
663 ),
664 )
665 );
666
667 // Validate the request.
668 $v = new Validator( $data, array() );
669 $v->rule( 'required', array( 'scenario', 'type', 'per_page', 'paged' ) );
670 if ( ! $v->validate() ) {
671 return new Response(
672 false,
673 array(
674 'message' => '',
675 )
676 );
677 }
678
679 $scan = Model_Scan::get_last();
680 $issues = $scan->to_array( $data['per_page'], $data['paged'], $data['type'] );
681
682 return new Response(
683 true,
684 array(
685 'issue' => $issues['issues_items'],
686 'ignored' => $issues['ignored_items'],
687 'paging' => $issues['paging'],
688 'count' => $issues['count'],
689 )
690 );
691 }
692
693 /**
694 * Update the run background state for active scan.
695 *
696 * @param Request $request Request object.
697 *
698 * @return Response Response object.
699 * @defender_route
700 */
701 public function update_background( Request $request ): Response {
702 $data = $request->get_data(
703 array(
704 'run_background' => array(
705 'type' => 'boolean',
706 'sanitize' => 'rest_sanitize_boolean',
707 ),
708 )
709 );
710
711 $run_background = $data['run_background'] ?? false;
712 $scan = Model_Scan::get_active();
713
714 if ( is_object( $scan ) && $run_background ) {
715 set_site_transient( 'defender_run_background', $scan->id, HOUR_IN_SECONDS * 2 );
716
717 return new Response(
718 true,
719 array(
720 'scan' => $scan->to_array(),
721 )
722 );
723 }
724
725 return new Response(
726 false,
727 array(
728 'message' => esc_html__( 'No active scan found', 'defender-security' ),
729 )
730 );
731 }
732
733 /**
734 * Render main page.
735 *
736 * @return void
737 */
738 public function main_view(): void {
739 $this->render( 'main' );
740 }
741
742 /**
743 * Enqueues scripts and styles for this page.
744 * Only enqueues assets if the page is active.
745 */
746 public function enqueue_assets() {
747 if ( ! $this->is_page_active() ) {
748 return;
749 }
750 wp_localize_script( 'def-scan', 'scan', $this->data_frontend() );
751 wp_enqueue_script( 'def-scan' );
752 wp_enqueue_script( 'clipboard' );
753 $this->enqueue_main_assets();
754 }
755
756 /**
757 * Converts the current object state to an array.
758 *
759 * @return array The array representation of the object.
760 */
761 public function to_array(): array {
762 $scan = Model_Scan::get_active();
763 $last = Model_Scan::get_last();
764 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
765 $scan = null;
766 } else {
767 $scan = is_object( $scan ) ? $scan->to_array() : $last->to_array();
768 }
769
770 return array_merge(
771 array(
772 'scan' => $scan,
773 'report' => array(
774 'enabled' => true,
775 'frequency' => 'weekly',
776 ),
777 ),
778 $this->dump_routes_and_nonces()
779 );
780 }
781
782 /**
783 * Removes settings for all submodules.
784 */
785 public function remove_settings(): void {
786 ( new Scan_Settings() )->delete();
787 }
788
789 /**
790 * Delete all the data & the cache.
791 */
792 public function remove_data(): void {
793 delete_site_option( Model_Scan::IGNORE_INDEXER );
794 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
795 delete_site_transient( Plugin_Integrity::$org_slugs );
796 delete_site_transient( Plugin_Integrity::$org_responses );
797 }
798
799 /**
800 * Provides data for the frontend.
801 *
802 * @return array An array of data for the frontend.
803 */
804 public function data_frontend(): array {
805 $scan = Model_Scan::get_active();
806 $last = Model_Scan::get_last();
807 $per_page = 10;
808 $paged = 1;
809 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
810 $scan = null;
811 } else {
812 $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged );
813 }
814 $settings = new Scan_Settings();
815 $report = wd_di()->get( Malware_Report::class );
816 $report_text = esc_html__( 'Automatic scans are disabled', 'defender-security' );
817 if ( $settings->scheduled_scanning && isset( $settings->frequency ) ) {
818 $report_text = sprintf(
819 /* translators: 1. Line break tag. 2. Frequency value. */
820 esc_html__( 'Automatic scans are %1$srunning %2$s', 'defender-security' ),
821 '<br/>',
822 $settings->frequency
823 );
824 }
825
826 $misc['outdated_period'] = \WP_Defender\Behavior\Scan\Abandoned_Plugin::get_outdated_period();
827 $misc['labels'] = $settings->labels();
828 $misc['days_of_week'] = $this->get_days_of_week();
829 $misc['times_of_day'] = $this->get_times();
830 $misc['timezone_text'] = sprintf(
831 /* translators: 1. Timezone. 2. Time. */
832 esc_html__(
833 'Your timezone is set to %1$s, so your current time is %2$s.',
834 'defender-security'
835 ),
836 '<strong>' . wp_timezone_string() . '</strong>',
837 '<strong>' . wp_date( 'H:i', time() ) . '</strong>'
838 );
839
840 // Todo: add logic for deactivated scan settings. Maybe display some notice.
841 $data = array(
842 'scan' => $scan,
843 'settings' => $settings->export(),
844 'report' => $report_text,
845 'active_tools' => array(
846 'integrity_check' => $settings->integrity_check,
847 'check_known_vuln' => $settings->check_known_vuln,
848 'scan_malware' => $settings->scan_malware,
849 'scheduled_scanning' => $settings->scheduled_scanning,
850 'check_abandoned_plugin' => $settings->check_abandoned_plugin,
851 ),
852 'notification' => $report->to_string(),
853 'next_run' => $report->get_next_run_as_string(),
854 'misc' => $misc,
855 'upsell' => array(
856 'scan' => $this->get_scan_upsell( 'scan' ),
857 ),
858 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
859 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
860 );
861
862 if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
863 $data['quarantine'] = $this->quarantine_controller->data_frontend();
864 }
865
866 return array_merge( $data, $this->dump_routes_and_nonces() );
867 }
868
869 /**
870 * Imports data into the model.
871 *
872 * @param array $data Data to be imported into the model.
873 */
874 public function import_data( array $data ) {
875 $model = $this->model;
876 if ( empty( $data ) ) {
877 $model->scheduled_scanning = false;
878 $model->frequency = 'weekly';
879 $model->day_n = 1;
880 $model->day = 'sunday';
881 $model->time = '4:00';
882 $model->save();
883 } else {
884 $model->import( $data );
885 if ( $model->validate() ) {
886 $model->save();
887 }
888 }
889 }
890
891 /**
892 * Checks if any scan is active.
893 *
894 * @param bool $is_pro Indicates if the product is a pro version.
895 *
896 * @return bool True if any scan is active, false otherwise.
897 */
898 private function is_any_active( bool $is_pro ): bool {
899 $settings = new Scan_Settings();
900 $file_change_check = $settings->is_checked_any_file_change_types();
901
902 if ( $is_pro ) {
903 // Pro version. Check all parent types.
904 return $file_change_check || $settings->check_known_vuln || $settings->scan_malware;
905 } else {
906 // Free version:
907 // Check the 'File change detection' type because only it's available with nested types.
908 // Check the Abandoned plugin type.
909 return $file_change_check || $settings->check_abandoned_plugin;
910 }
911 }
912
913 /**
914 * Exports strings.
915 *
916 * @return array An array of strings.
917 */
918 public function export_strings(): array {
919 $strings = array();
920 if ( $this->is_any_active( $this->is_pro ) ) {
921 $strings[] = esc_html__( 'Active', 'defender-security' );
922 } else {
923 $strings[] = esc_html__( 'Inactive', 'defender-security' );
924 }
925
926 $scan_report = new Malware_Report();
927 $scan_notification = new Malware_Notification();
928 if ( 'enabled' === $scan_notification->status ) {
929 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
930 }
931 if ( $this->is_pro && 'enabled' === $scan_report->status ) {
932 $strings[] = sprintf(
933 /* translators: %s: Frequency value. */
934 esc_html__( 'Email reports sending %s', 'defender-security' ),
935 $scan_report->frequency
936 );
937 } elseif ( ! $this->is_pro ) {
938 $strings[] = sprintf(
939 /* translators: %s: Html for Pro-tag. */
940 esc_html__( 'Email report inactive %s', 'defender-security' ),
941 '<span class="sui-tag sui-tag-pro">Pro</span>'
942 );
943 }
944
945 return $strings;
946 }
947
948 /**
949 * Generates configuration strings based on the provided configuration and
950 * whether the product is a pro version.
951 *
952 * @param array $config Configuration data.
953 * @param bool $is_pro Indicates if the product is a pro version.
954 *
955 * @return array Returns an array of configuration strings.
956 */
957 public function config_strings( array $config, bool $is_pro ): array {
958 $strings = array();
959 $strings[] = $this->service->is_any_scan_active( $config )
960 ? esc_html__( 'Active', 'defender-security' )
961 : esc_html__( 'Inactive', 'defender-security' );
962
963 if ( 'enabled' === $config['notification'] ) {
964 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
965 }
966 if ( $is_pro && 'enabled' === $config['report'] ) {
967 $strings[] = sprintf(
968 /* translators: %s: Frequency value. */
969 esc_html__( 'Email reports sending %s', 'defender-security' ),
970 $config['frequency']
971 );
972 } elseif ( ! $is_pro ) {
973 $strings[] = sprintf(
974 /* translators: %s: Html for Pro-tag. */
975 esc_html__( 'Email report inactive %s', 'defender-security' ),
976 '<span class="sui-tag sui-tag-pro">Pro</span>'
977 );
978 }
979
980 return $strings;
981 }
982
983 /**
984 * Triggers the asynchronous scan.
985 *
986 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
987 *
988 * @return void
989 */
990 public function do_async_scan( string $type ): void {
991 wd_di()->get( Model_Scan::class )->delete_idle();
992 // Delete the slug from the previous scan.
993 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
994
995 as_enqueue_async_action(
996 'defender/async_scan',
997 array(
998 'type' => $type,
999 ),
1000 'defender'
1001 );
1002 }
1003
1004 /**
1005 * Clear completed action scheduler logs.
1006 *
1007 * @return void
1008 * @since 2.6.5
1009 */
1010 public function clear_scan_logs(): void {
1011 $scan_component = wd_di()->get( Scan_Component::class );
1012 $result = $scan_component::clear_logs();
1013
1014 if ( isset( $result['error'] ) ) {
1015 $this->log( 'WP CRON Error : ' . $result['error'], self::SCAN_LOG );
1016 }
1017 }
1018
1019 /**
1020 * When user session is expired and scan is running, then don't login via heartbeat modal.
1021 *
1022 * @param array $response The no-priv Heartbeat response.
1023 * @param string $screen_id The screen id.
1024 *
1025 * @return mixed
1026 * @since 3.11.0
1027 */
1028 public function nopriv_heartbeat( $response, $screen_id ) {
1029 if ( false !== strpos( $screen_id, $this->slug ) ) {
1030 $scan = Model_Scan::get_active();
1031
1032 if ( is_object( $scan ) ) {
1033 $response['wp-auth-check'] = true;
1034 }
1035 }
1036
1037 return $response;
1038 }
1039
1040 /**
1041 * Triggers and send analytics data on scan completed.
1042 *
1043 * @param int $action_id Action ID.
1044 *
1045 * @return void
1046 */
1047 public function scan_completed_analytics( $action_id ) {
1048 if ( 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group() ) {
1049 $scan_analytics = wd_di()->get( Scan_Analytics::class );
1050
1051 $scan_model = wd_di()->get( Model_Scan::class );
1052 $analytics_data = $scan_analytics->scan_completed( $scan_model );
1053 if ( empty( $analytics_data ) ) {
1054 return;
1055 }
1056
1057 $this->track_feature(
1058 $analytics_data['event'],
1059 $analytics_data['data']
1060 );
1061 }
1062 }
1063 }
1064