PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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.4.0, at src/controller/class-scan.php

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