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

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