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

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

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