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

1,035 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 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->run_scan_mechanisms_from( 'scan' );
164
165 return new Response(
166 true,
167 array(
168 'status' => $model->status,
169 'status_text' => $model->get_status_text(),
170 'percent' => 0,
171 )
172 );
173 }
174
175 return new Response(
176 false,
177 array(
178 'message' => esc_html__( 'A scan is already in progress', 'defender-security' ),
179 )
180 );
181 }
182
183 /**
184 * Use this for self ping, so it can both run in background and active mode with good performance.
185 *
186 * @return void
187 * @defender_route
188 * @is_public
189 */
190 public function process() {
191 $lock_filename = $this->service->get_lock_filename();
192 if ( $this->service->has_lock( $lock_filename ) ) {
193 $this->log( 'Fallback as already a process is running', self::SCAN_LOG );
194
195 return;
196 }
197
198 // This creates file lock, for make sure only 1 process run as a time.
199 $this->service->create_lock( $lock_filename );
200 // Check if the ping is from self or not.
201 $ret = $this->service->process();
202 $this->log( 'process done, queue for next', self::SCAN_LOG );
203 if ( false === $ret ) {
204 // Ping self.
205 $this->log( 'Scan not done, pinging', self::SCAN_LOG );
206 $this->service->remove_lock( $lock_filename );
207 $this->process();
208 } else {
209 $this->queue_to_sync_with_hub();
210 $this->service->remove_lock( $lock_filename );
211 }
212 }
213
214 /**
215 * Query status.
216 *
217 * @return Response
218 * @defender_route
219 * @defender_redirect
220 */
221 public function status(): Response {
222 $idle_scan = wd_di()->get( Model_Scan::class )->get_idle();
223
224 if ( is_object( $idle_scan ) ) {
225 $this->service->update_idle_scan_status();
226
227 return new Response( false, $idle_scan->to_array() );
228 }
229
230 $checksum_issue = get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 'false' );
231 $checksum_scan = Model_Scan::get_core_check();
232 if ( 'false' !== $checksum_issue && is_object( $checksum_scan ) ) {
233 $this->service->update_idle_scan_status_by_checksum_issue( $checksum_scan );
234
235 return new Response( false, $checksum_scan->to_array() );
236 }
237
238 $scan = Model_Scan::get_active();
239 if ( is_object( $scan ) ) {
240
241 return new Response( false, $scan->to_array() );
242 }
243 $scan = Model_Scan::get_last();
244 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
245 $response = $scan->to_array();
246 $response['message'] = __( 'Malware scan completed successfully!', 'defender-security' );
247 return new Response( true, $response );
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'], $item->owner );
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 ! is_array( $items )
466 || 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 $sync_hub = $scan->ignore_issue( (int) $id );
484 } elseif ( 'unignore' === $intention ) {
485 $sync_hub = $scan->unignore_issue( (int) $id );
486 } elseif ( 'delete' === $intention ) {
487 $item = $scan->get_issue( (int) $id );
488 // Work with every item.
489 if ( is_object( $item ) && $item->has_method( $intention ) ) {
490 $item_result = $item->delete();
491 if ( is_wp_error( $item_result ) ) {
492 $none_delete_items[] = $item_result->get_error_message();
493 } elseif ( isset( $item_result['type_notice'] ) ) {
494 return new Response( true, $item_result );
495 } elseif ( isset( $item_result['collect_type'] ) ) {
496 $is_delete = true;
497 $delete_items[] = $item_result['message'];
498 }
499 // If there is any error, no need to sync data.
500 $sync_hub = true;
501 }
502 }
503 }
504
505 if ( $sync_hub ) {
506 $this->queue_to_sync_with_hub();
507 }
508
509 $result = array();
510 if ( array() !== $none_delete_items ) {
511 $result['message'] = sprintf(
512 /* translators: %s: Vulnerability item(es) */
513 _n(
514 'Defender doesn\'t have enough permission to remove this file: %s',
515 'Defender doesn\'t have enough permission to remove these files: %s',
516 count( $none_delete_items ),
517 'defender-security'
518 ),
519 '<pre>' . implode( PHP_EOL, $none_delete_items ) . '</pre>'
520 );
521 } elseif ( $is_delete ) {
522 $result['message'] = sprintf(
523 /* translators: %s: Vulnerability item(es) */
524 esc_html__( '%s has (have) been deleted', 'defender-security' ),
525 implode( ', ', $delete_items )
526 );
527 }
528 // Refresh scan instance.
529 $scan = Model_Scan::get_last();
530 $result['scan'] = $scan->to_array();
531
532 return new Response( array() === $none_delete_items, $result );
533 }
534
535 /**
536 * Save settings.
537 *
538 * @param Request $request The request object containing new settings data.
539 *
540 * @return Response
541 * @since 2.7.0 Add Scheduled Scanning to Malware settings and hide it on Malware Scanning - Reporting.
542 * Also, the backward compatibility of settings for Scan and Malware_Report models.
543 * @defender_route
544 */
545 public function save_settings( Request $request ): Response {
546 $data = $request->get_data_by_model( $this->model );
547 // Case#1: enable all child options, if parent and all child options are disabled, so that there is no notice when saving.
548 if (
549 ! $data['integrity_check']
550 && ! $data['check_core']
551 && ! $data['check_plugins']
552 ) {
553 $data['check_core'] = true;
554 $data['check_plugins'] = true;
555 }
556
557 // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice.
558 if ( $data['scan_malware'] && ! $data['integrity_check'] ) {
559 $response = array(
560 'type_notice' => 'info',
561 'message' => sprintf(
562 /* translators: 1. Open tag. 2. Close tag. 3. Open tag. 4. Close tag. */
563 esc_html__(
564 '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.',
565 'defender-security'
566 ),
567 '<strong>',
568 '</strong>',
569 '<strong>',
570 '</strong>'
571 ),
572 );
573 } else {
574 // Prepare response message for usual successful case.
575 $response = array(
576 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
577 'auto_close' => true,
578 );
579 }
580 // Additional cases are in the Scan model.
581 $report_change = false;
582 // If 'Scheduled Scanning' is checked then need to change Malware_Report.
583 if ( true === $data['scheduled_scanning'] ) {
584 $report = new Malware_Report();
585 $report_change = true;
586 $report->frequency = $data['frequency'];
587 $report->day = $data['day'];
588 $report->day_n = (int) $data['day_n'];
589 $report->time = $data['time'];
590 // Disable 'Scheduled Scanning'.
591 } elseif ( true === $this->model->scheduled_scanning && false === $data['scheduled_scanning'] ) {
592 $report = new Malware_Report();
593 $report_change = true;
594 $report->status = \WP_Defender\Model\Notification::STATUS_DISABLED;
595 }
596
597 $before_import_schedule = $this->model->quarantine_expire_schedule;
598
599 $this->model->import( $data );
600 if ( $this->model->validate() ) {
601
602 if ( class_exists( 'WP_Defender\Component\Quarantine' ) ) {
603 $quarantine_component = wd_di()->get( Quarantine_Component::class );
604 $quarantine_component->reschedule_file_expiry_cron(
605 $before_import_schedule,
606 $data['quarantine_expire_schedule']
607 );
608 }
609
610 // Todo: need to disable Malware_Notification & Malware_Report if all scan settings are deactivated?
611 $this->model->save();
612 // Save Report's changes.
613 if ( $report_change ) {
614 $report->save();
615 }
616 Config_Hub_Helper::set_clear_active_flag();
617
618 return new Response(
619 true,
620 array_merge( $response, $this->data_frontend() )
621 );
622 } else {
623 return new Response(
624 false,
625 array_merge(
626 array(
627 'message' => $this->model->get_formatted_errors(),
628 ),
629 $this->data_frontend()
630 )
631 );
632 }
633 }
634
635 /**
636 * Get the issues mainly for pagination request.
637 *
638 * @param Request $request The request object.
639 *
640 * @return Response
641 * @defender_route
642 */
643 public function get_issues( Request $request ): Response {
644 $data = $request->get_data(
645 array(
646 'scenario' => array(
647 'type' => 'string',
648 'sanitize' => 'sanitize_text_field',
649 ),
650 'type' => array(
651 'type' => 'string',
652 'sanitize' => 'sanitize_text_field',
653 ),
654 'per_page' => array(
655 'type' => 'string',
656 'sanitize' => 'intval',
657 ),
658 'paged' => array(
659 'type' => 'int',
660 'sanitize' => 'intval',
661 ),
662 )
663 );
664
665 // Validate the request.
666 $v = new Validator( $data, array() );
667 $v->rule( 'required', array( 'scenario', 'type', 'per_page', 'paged' ) );
668 if ( ! $v->validate() ) {
669 return new Response(
670 false,
671 array(
672 'message' => '',
673 )
674 );
675 }
676
677 $scan = Model_Scan::get_last();
678 $issues = $scan->to_array( $data['per_page'], $data['paged'], $data['type'] );
679
680 return new Response(
681 true,
682 array(
683 'issue' => $issues['issues_items'],
684 'ignored' => $issues['ignored_items'],
685 'paging' => $issues['paging'],
686 'count' => $issues['count'],
687 )
688 );
689 }
690
691 /**
692 * Render main page.
693 *
694 * @return void
695 */
696 public function main_view(): void {
697 $this->render( 'main' );
698 }
699
700 /**
701 * Enqueues scripts and styles for this page.
702 * Only enqueues assets if the page is active.
703 */
704 public function enqueue_assets() {
705 if ( ! $this->is_page_active() ) {
706 return;
707 }
708 wp_localize_script( 'def-scan', 'scan', $this->data_frontend() );
709 wp_enqueue_script( 'def-scan' );
710 wp_enqueue_script( 'clipboard' );
711 $this->enqueue_main_assets();
712 }
713
714 /**
715 * Converts the current object state to an array.
716 *
717 * @return array The array representation of the object.
718 */
719 public function to_array(): array {
720 $scan = Model_Scan::get_active();
721 $last = Model_Scan::get_last();
722 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
723 $scan = null;
724 } else {
725 $scan = is_object( $scan ) ? $scan->to_array() : $last->to_array();
726 }
727
728 return array_merge(
729 array(
730 'scan' => $scan,
731 'report' => array(
732 'enabled' => true,
733 'frequency' => 'weekly',
734 ),
735 ),
736 $this->dump_routes_and_nonces()
737 );
738 }
739
740 /**
741 * Removes settings for all submodules.
742 */
743 public function remove_settings(): void {
744 ( new Scan_Settings() )->delete();
745 }
746
747 /**
748 * Delete all the data & the cache.
749 */
750 public function remove_data(): void {
751 delete_site_option( Model_Scan::IGNORE_INDEXER );
752 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
753 delete_site_transient( Plugin_Integrity::$org_slugs );
754 delete_site_transient( Plugin_Integrity::$org_responses );
755 }
756
757 /**
758 * Provides data for the frontend.
759 *
760 * @return array An array of data for the frontend.
761 */
762 public function data_frontend(): array {
763 $scan = Model_Scan::get_active();
764 $last = Model_Scan::get_last();
765 $per_page = 10;
766 $paged = 1;
767 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
768 $scan = null;
769 } else {
770 $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged );
771 }
772 $settings = new Scan_Settings();
773 $report = wd_di()->get( Malware_Report::class );
774 $report_text = esc_html__( 'Automatic scans are disabled', 'defender-security' );
775 if ( $settings->scheduled_scanning && isset( $settings->frequency ) ) {
776 $report_text = sprintf(
777 /* translators: 1. Line break tag. 2. Frequency value. */
778 esc_html__( 'Automatic scans are %1$srunning %2$s', 'defender-security' ),
779 '<br/>',
780 $settings->frequency
781 );
782 }
783 $misc = array(
784 'outdated_period' => \WP_Defender\Behavior\Scan\Abandoned_Plugin::get_outdated_period(),
785 'labels' => $settings->labels(),
786 'days_of_week' => $this->get_days_of_week(),
787 'times_of_day' => $this->get_times(),
788 );
789 $misc['timezone_text'] = sprintf(
790 /* translators: 1. Timezone. 2. Time. */
791 esc_html__(
792 'Your timezone is set to %1$s, so your current time is %2$s.',
793 'defender-security'
794 ),
795 '<strong>' . wp_timezone_string() . '</strong>',
796 '<strong>' . wp_date( 'H:i', time() ) . '</strong>'
797 );
798
799 // Todo: add logic for deactivated scan settings. Maybe display some notice.
800 $data = array(
801 'scan' => $scan,
802 'settings' => $settings->export(),
803 'report' => $report_text,
804 'active_tools' => array(
805 'integrity_check' => $settings->integrity_check,
806 'check_known_vuln' => $settings->check_known_vuln,
807 'scan_malware' => $settings->scan_malware,
808 'scheduled_scanning' => $settings->scheduled_scanning,
809 'check_abandoned_plugin' => $settings->check_abandoned_plugin,
810 ),
811 'notification' => $report->to_string(),
812 'next_run' => $report->get_next_run_as_string(),
813 'misc' => $misc,
814 'upsell' => array(
815 'scan' => $this->get_scan_upsell( 'scan' ),
816 ),
817 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
818 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
819 );
820
821 if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
822 $data['quarantine'] = $this->quarantine_controller->data_frontend();
823 }
824
825 return array_merge( $data, $this->dump_routes_and_nonces() );
826 }
827
828 /**
829 * Imports data into the model.
830 *
831 * @param array $data Data to be imported into the model.
832 */
833 public function import_data( array $data ) {
834 $model = $this->model;
835 if ( array() === $data ) {
836 $model->scheduled_scanning = false;
837 $model->frequency = 'weekly';
838 $model->day_n = 1;
839 $model->day = 'sunday';
840 $model->time = '4:00';
841 $model->save();
842 } else {
843 $model->import( $data );
844 if ( $model->validate() ) {
845 $model->save();
846 }
847 }
848 }
849
850 /**
851 * Checks if any scan is active.
852 *
853 * @param bool $is_pro Indicates if the product is a pro version.
854 *
855 * @return bool True if any scan is active, false otherwise.
856 */
857 private function is_any_active( bool $is_pro ): bool {
858 $settings = new Scan_Settings();
859 $file_change_check = $settings->is_checked_any_file_change_types();
860
861 if ( $is_pro ) {
862 // Pro version. Check all parent types.
863 return $file_change_check || $settings->check_known_vuln || $settings->scan_malware;
864 } else {
865 // Free version:
866 // Check the 'File change detection' type because only it's available with nested types.
867 // Check the Abandoned plugin type.
868 return $file_change_check || $settings->check_abandoned_plugin;
869 }
870 }
871
872 /**
873 * Exports strings.
874 *
875 * @return array An array of strings.
876 */
877 public function export_strings(): array {
878 $strings = array();
879 if ( $this->is_any_active( $this->is_pro ) ) {
880 $strings[] = esc_html__( 'Active', 'defender-security' );
881 } else {
882 $strings[] = esc_html__( 'Inactive', 'defender-security' );
883 }
884
885 $scan_report = new Malware_Report();
886 $scan_notification = new Malware_Notification();
887 if ( 'enabled' === $scan_notification->status ) {
888 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
889 }
890 if ( $this->is_pro && 'enabled' === $scan_report->status ) {
891 $strings[] = sprintf(
892 /* translators: %s: Frequency value. */
893 esc_html__( 'Email reports sending %s', 'defender-security' ),
894 $scan_report->frequency
895 );
896 } elseif ( ! $this->is_pro ) {
897 $strings[] = sprintf(
898 /* translators: %s: Html for Pro-tag. */
899 esc_html__( 'Email report inactive %s', 'defender-security' ),
900 '<span class="sui-tag sui-tag-pro">Pro</span>'
901 );
902 }
903
904 return $strings;
905 }
906
907 /**
908 * Generates configuration strings based on the provided configuration and
909 * whether the product is a pro version.
910 *
911 * @param array $config Configuration data.
912 * @param bool $is_pro Indicates if the product is a pro version.
913 *
914 * @return array Returns an array of configuration strings.
915 */
916 public function config_strings( array $config, bool $is_pro ): array {
917 $strings = array();
918 $strings[] = $this->service->is_any_scan_active( $config )
919 ? esc_html__( 'Active', 'defender-security' )
920 : esc_html__( 'Inactive', 'defender-security' );
921
922 if ( 'enabled' === $config['notification'] ) {
923 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
924 }
925 if ( $is_pro && 'enabled' === $config['report'] ) {
926 $strings[] = sprintf(
927 /* translators: %s: Frequency value. */
928 esc_html__( 'Email reports sending %s', 'defender-security' ),
929 $config['frequency']
930 );
931 } elseif ( ! $is_pro ) {
932 $strings[] = sprintf(
933 /* translators: %s: Html for Pro-tag. */
934 esc_html__( 'Email report inactive %s', 'defender-security' ),
935 '<span class="sui-tag sui-tag-pro">Pro</span>'
936 );
937 }
938
939 return $strings;
940 }
941
942 /**
943 * Run different scan actions based on the scan location.
944 *
945 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
946 *
947 * @return void
948 */
949 public function run_scan_mechanisms_from( $type ) {
950 $this->service->gather_actioned_plugin_details();
951 $this->do_async_scan( $type );
952 }
953
954 /**
955 * Triggers the asynchronous scan.
956 *
957 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
958 *
959 * @return void
960 */
961 public function do_async_scan( string $type ): void {
962 wd_di()->get( Model_Scan::class )->delete_idle();
963 // Delete the slug from the previous scan.
964 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
965
966 as_enqueue_async_action(
967 'defender/async_scan',
968 array(
969 'type' => $type,
970 ),
971 'defender'
972 );
973 }
974
975 /**
976 * Clear completed action scheduler logs.
977 *
978 * @return void
979 * @since 2.6.5
980 */
981 public function clear_scan_logs(): void {
982 $scan_component = wd_di()->get( Scan_Component::class );
983 $result = $scan_component::clear_logs();
984
985 if ( isset( $result['error'] ) ) {
986 $this->log( 'WP CRON Error : ' . $result['error'], self::SCAN_LOG );
987 }
988 }
989
990 /**
991 * When user session is expired and scan is running, then don't login via heartbeat modal.
992 *
993 * @param array $response The no-priv Heartbeat response.
994 * @param string $screen_id The screen id.
995 *
996 * @return mixed
997 * @since 3.11.0
998 */
999 public function nopriv_heartbeat( $response, $screen_id ) {
1000 if ( false !== strpos( $screen_id, $this->slug ) ) {
1001 $scan = Model_Scan::get_active();
1002
1003 if ( is_object( $scan ) ) {
1004 $response['wp-auth-check'] = true;
1005 }
1006 }
1007
1008 return $response;
1009 }
1010
1011 /**
1012 * Triggers and send analytics data on scan completed.
1013 *
1014 * @param int $action_id Action ID.
1015 *
1016 * @return void
1017 */
1018 public function scan_completed_analytics( $action_id ) {
1019 if ( 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group() ) {
1020 $scan_analytics = wd_di()->get( Scan_Analytics::class );
1021
1022 $scan_model = wd_di()->get( Model_Scan::class );
1023 $analytics_data = $scan_analytics->scan_completed( $scan_model );
1024 if ( array() === $analytics_data ) {
1025 return;
1026 }
1027
1028 $this->track_feature(
1029 $analytics_data['event'],
1030 $analytics_data['data']
1031 );
1032 }
1033 }
1034 }
1035