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

1,049 lines 28.4 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 if ( 'quarantine' === $intention ) {
388 $result = $item->quarantine( $data['parent_action'], $item->owner );
389 } elseif ( 'resolve' === $intention ) {
390 $result = $item->resolve();
391 } elseif ( 'delete' === $intention ) {
392 $result = $item->delete();
393 } elseif ( 'ignore' === $intention ) {
394 $result = $item->ignore();
395 } elseif ( 'unignore' === $intention ) {
396 $result = $item->unignore();
397 } elseif ( 'pull_src' === $intention ) {
398 $result = $item->pull_src();
399 }
400
401 // Maybe track.
402 if ( $this->is_tracking_active() ) {
403 $this->item_action_analytics( $item, $intention );
404 }
405
406 if ( is_wp_error( $result ) ) {
407 return new Response(
408 false,
409 array(
410 'message' => $result->get_error_message(),
411 )
412 );
413 } elseif ( isset( $result['type_notice'] ) ) {
414 return new Response(
415 true,
416 $result
417 );
418 } elseif ( isset( $result['url'] ) ) {
419 // Without message and interval args.
420 return new Response(
421 true,
422 array( 'redirect' => $result['url'] )
423 );
424 }
425
426 $this->queue_to_sync_with_hub();
427
428 // Refresh scan instance.
429 $scan = Model_Scan::get_last();
430
431 if ( $scan instanceof Model_Scan ) {
432 $result['scan'] = $scan->to_array();
433
434 $success = true;
435 if ( isset( $result['success'] ) && false === $result['success'] ) {
436 $success = false;
437 }
438
439 return new Response( $success, $result );
440 }
441 }
442 }
443
444 return new Response( false, array() );
445 }
446
447 /**
448 * Process for bulk action.
449 * There is no Update-intention because it is a lengthy process. There may not be enough execution time.
450 *
451 * @param Request $request Request object.
452 *
453 * @defender_route
454 * @return Response
455 */
456 public function bulk_action( Request $request ): Response {
457 $data = $request->get_data(
458 array(
459 'items' => array(
460 'type' => 'array',
461 'sanitize' => 'sanitize_text_field',
462 ),
463 'bulk' => array(
464 'type' => 'string',
465 'sanitize' => 'sanitize_text_field',
466 ),
467 )
468 );
469 $items = $data['items'] ?? array();
470 $intention = $data['bulk'] ?? false;
471
472 if (
473 ! is_array( $items )
474 || array() === $items
475 || ! in_array( $intention, array( 'ignore', 'unignore', 'delete' ), true )
476 ) {
477 return new Response( false, array() );
478 }
479 // Try to get Scan.
480 $scan = Model_Scan::get_last();
481 if ( ! is_object( $scan ) ) {
482 return new Response( false, array() );
483 }
484
485 $is_delete = false;
486 $delete_items = array();
487 $none_delete_items = array();
488 $sync_hub = false;
489 foreach ( $items as $id ) {
490 if ( 'ignore' === $intention ) {
491 $sync_hub = $scan->ignore_issue( (int) $id );
492 } elseif ( 'unignore' === $intention ) {
493 $sync_hub = $scan->unignore_issue( (int) $id );
494 } elseif ( 'delete' === $intention ) {
495 $item = $scan->get_issue( (int) $id );
496 // Work with every item.
497 if ( is_object( $item ) && $item->has_method( $intention ) ) {
498 $item_result = $item->delete();
499 if ( is_wp_error( $item_result ) ) {
500 $none_delete_items[] = $item_result->get_error_message();
501 } elseif ( isset( $item_result['type_notice'] ) ) {
502 return new Response( true, $item_result );
503 } elseif ( isset( $item_result['collect_type'] ) ) {
504 $is_delete = true;
505 $delete_items[] = $item_result['message'];
506 }
507 // If there is any error, no need to sync data.
508 $sync_hub = true;
509 }
510 }
511 }
512
513 if ( $sync_hub ) {
514 $this->queue_to_sync_with_hub();
515 }
516
517 $result = array();
518 if ( array() !== $none_delete_items ) {
519 $result['message'] = sprintf(
520 /* translators: %s: Vulnerability item(es) */
521 _n(
522 'Defender doesn\'t have enough permission to remove this file: %s',
523 'Defender doesn\'t have enough permission to remove these files: %s',
524 count( $none_delete_items ),
525 'defender-security'
526 ),
527 '<pre>' . implode( PHP_EOL, $none_delete_items ) . '</pre>'
528 );
529 } elseif ( $is_delete ) {
530 $result['message'] = sprintf(
531 /* translators: %s: Vulnerability item(es) */
532 esc_html__( '%s has (have) been deleted', 'defender-security' ),
533 implode( ', ', $delete_items )
534 );
535 }
536 // Refresh scan instance.
537 $scan = Model_Scan::get_last();
538 $result['scan'] = $scan->to_array();
539
540 return new Response( array() === $none_delete_items, $result );
541 }
542
543 /**
544 * Save settings.
545 *
546 * @param Request $request The request object containing new settings data.
547 *
548 * @return Response
549 * @since 2.7.0 Add Scheduled Scanning to Malware settings and hide it on Malware Scanning - Reporting.
550 * Also, the backward compatibility of settings for Scan and Malware_Report models.
551 * @defender_route
552 */
553 public function save_settings( Request $request ): Response {
554 $data = $request->get_data_by_model( $this->model );
555 // Case#1: enable all child options, if parent and all child options are disabled, so that there is no notice when saving.
556 if (
557 ! $data['integrity_check']
558 && ! $data['check_core']
559 && ! $data['check_plugins']
560 ) {
561 $data['check_core'] = true;
562 $data['check_plugins'] = true;
563 }
564
565 // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice.
566 if ( $data['scan_malware'] && ! $data['integrity_check'] ) {
567 $response = array(
568 'type_notice' => 'info',
569 'message' => sprintf(
570 /* translators: 1. Open tag. 2. Close tag. 3. Open tag. 4. Close tag. */
571 esc_html__(
572 '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.',
573 'defender-security'
574 ),
575 '<strong>',
576 '</strong>',
577 '<strong>',
578 '</strong>'
579 ),
580 );
581 } else {
582 // Prepare response message for usual successful case.
583 $response = array(
584 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
585 'auto_close' => true,
586 );
587 }
588 // Additional cases are in the Scan model.
589 $report_change = false;
590 // If 'Scheduled Scanning' is checked then need to change Malware_Report.
591 if ( true === $data['scheduled_scanning'] ) {
592 $report = new Malware_Report();
593 $report_change = true;
594 $report->frequency = $data['frequency'];
595 $report->day = $data['day'];
596 $report->day_n = (int) $data['day_n'];
597 $report->time = $data['time'];
598 // Disable 'Scheduled Scanning'.
599 } elseif ( true === $this->model->scheduled_scanning && false === $data['scheduled_scanning'] ) {
600 $report = new Malware_Report();
601 $report_change = true;
602 $report->status = \WP_Defender\Model\Notification::STATUS_DISABLED;
603 }
604
605 $before_import_schedule = $this->model->quarantine_expire_schedule;
606
607 $this->model->import( $data );
608 if ( $this->model->validate() ) {
609
610 if ( class_exists( 'WP_Defender\Component\Quarantine' ) ) {
611 $quarantine_component = wd_di()->get( Quarantine_Component::class );
612 $quarantine_component->reschedule_file_expiry_cron(
613 $before_import_schedule,
614 $data['quarantine_expire_schedule']
615 );
616 }
617
618 // Todo: need to disable Malware_Notification & Malware_Report if all scan settings are deactivated?
619 $this->model->save();
620 // Save Report's changes.
621 if ( $report_change ) {
622 $report->save();
623 }
624 Config_Hub_Helper::set_clear_active_flag();
625
626 return new Response(
627 true,
628 array_merge( $response, $this->data_frontend() )
629 );
630 } else {
631 return new Response(
632 false,
633 array_merge(
634 array(
635 'message' => $this->model->get_formatted_errors(),
636 ),
637 $this->data_frontend()
638 )
639 );
640 }
641 }
642
643 /**
644 * Get the issues mainly for pagination request.
645 *
646 * @param Request $request The request object.
647 *
648 * @return Response
649 * @defender_route
650 */
651 public function get_issues( Request $request ): Response {
652 $data = $request->get_data(
653 array(
654 'scenario' => array(
655 'type' => 'string',
656 'sanitize' => 'sanitize_text_field',
657 ),
658 'type' => array(
659 'type' => 'string',
660 'sanitize' => 'sanitize_text_field',
661 ),
662 'per_page' => array(
663 'type' => 'string',
664 'sanitize' => 'intval',
665 ),
666 'paged' => array(
667 'type' => 'int',
668 'sanitize' => 'intval',
669 ),
670 )
671 );
672
673 // Validate the request.
674 $v = new Validator( $data, array() );
675 $v->rule( 'required', array( 'scenario', 'type', 'per_page', 'paged' ) );
676 if ( ! $v->validate() ) {
677 return new Response(
678 false,
679 array(
680 'message' => '',
681 )
682 );
683 }
684
685 $scan = Model_Scan::get_last();
686 $issues = $scan->to_array( $data['per_page'], $data['paged'], $data['type'] );
687
688 return new Response(
689 true,
690 array(
691 'issue' => $issues['issues_items'],
692 'ignored' => $issues['ignored_items'],
693 'paging' => $issues['paging'],
694 'count' => $issues['count'],
695 )
696 );
697 }
698
699 /**
700 * Render main page.
701 *
702 * @return void
703 */
704 public function main_view(): void {
705 $this->render( 'main' );
706 }
707
708 /**
709 * Enqueues scripts and styles for this page.
710 * Only enqueues assets if the page is active.
711 */
712 public function enqueue_assets() {
713 if ( ! $this->is_page_active() ) {
714 return;
715 }
716 wp_localize_script( 'def-scan', 'scan', $this->data_frontend() );
717 wp_enqueue_script( 'def-scan' );
718 wp_enqueue_script( 'clipboard' );
719 $this->enqueue_main_assets();
720 }
721
722 /**
723 * Converts the current object state to an array.
724 *
725 * @return array The array representation of the object.
726 */
727 public function to_array(): array {
728 $scan = Model_Scan::get_active();
729 $last = Model_Scan::get_last();
730 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
731 $scan = null;
732 } else {
733 $scan = is_object( $scan ) ? $scan->to_array() : $last->to_array();
734 }
735
736 return array_merge(
737 array(
738 'scan' => $scan,
739 'report' => array(
740 'enabled' => true,
741 'frequency' => 'weekly',
742 ),
743 ),
744 $this->dump_routes_and_nonces()
745 );
746 }
747
748 /**
749 * Removes settings for all submodules.
750 */
751 public function remove_settings(): void {
752 ( new Scan_Settings() )->delete();
753 }
754
755 /**
756 * Delete all the data & the cache.
757 */
758 public function remove_data(): void {
759 delete_site_option( Model_Scan::IGNORE_INDEXER );
760 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
761 delete_site_transient( Plugin_Integrity::$org_slugs );
762 delete_site_transient( Plugin_Integrity::$org_responses );
763 }
764
765 /**
766 * Provides data for the frontend.
767 *
768 * @return array An array of data for the frontend.
769 */
770 public function data_frontend(): array {
771 $scan = Model_Scan::get_active();
772 $last = Model_Scan::get_last();
773 $per_page = 10;
774 $paged = 1;
775 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
776 $scan = null;
777 } else {
778 $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged );
779 }
780 $settings = new Scan_Settings();
781 $report = wd_di()->get( Malware_Report::class );
782 $report_text = esc_html__( 'Automatic scans are disabled', 'defender-security' );
783 if ( $settings->scheduled_scanning && isset( $settings->frequency ) ) {
784 $report_text = sprintf(
785 /* translators: 1. Line break tag. 2. Frequency value. */
786 esc_html__( 'Automatic scans are %1$srunning %2$s', 'defender-security' ),
787 '<br/>',
788 $settings->frequency
789 );
790 }
791 $misc = array(
792 'outdated_period' => \WP_Defender\Behavior\Scan\Abandoned_Plugin::get_outdated_period(),
793 'labels' => $settings->labels(),
794 'days_of_week' => $this->get_days_of_week(),
795 'times_of_day' => $this->get_times(),
796 );
797 $misc['timezone_text'] = sprintf(
798 /* translators: 1. Timezone. 2. Time. */
799 esc_html__(
800 'Your timezone is set to %1$s, so your current time is %2$s.',
801 'defender-security'
802 ),
803 '<strong>' . wp_timezone_string() . '</strong>',
804 '<strong>' . wp_date( 'H:i', time() ) . '</strong>'
805 );
806
807 // Todo: add logic for deactivated scan settings. Maybe display some notice.
808 $data = array(
809 'scan' => $scan,
810 'settings' => $settings->export(),
811 'report' => $report_text,
812 'active_tools' => array(
813 'integrity_check' => $settings->integrity_check,
814 'check_known_vuln' => $settings->check_known_vuln,
815 'scan_malware' => $settings->scan_malware,
816 'scheduled_scanning' => $settings->scheduled_scanning,
817 'check_abandoned_plugin' => $settings->check_abandoned_plugin,
818 ),
819 'notification' => $report->to_string(),
820 'next_run' => $report->get_next_run_as_string(),
821 'misc' => $misc,
822 'upsell' => array(
823 'scan' => $this->get_scan_upsell( 'scan' ),
824 ),
825 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
826 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
827 );
828
829 if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
830 $data['quarantine'] = $this->quarantine_controller->data_frontend();
831 }
832
833 return array_merge( $data, $this->dump_routes_and_nonces() );
834 }
835
836 /**
837 * Imports data into the model.
838 *
839 * @param array $data Data to be imported into the model.
840 */
841 public function import_data( array $data ) {
842 $model = $this->model;
843 if ( array() === $data ) {
844 $model->scheduled_scanning = false;
845 $model->frequency = 'weekly';
846 $model->day_n = 1;
847 $model->day = 'sunday';
848 $model->time = '4:00';
849 $model->save();
850 } else {
851 $model->import( $data );
852 if ( $model->validate() ) {
853 $model->save();
854 }
855 }
856 }
857
858 /**
859 * Checks if any scan is active.
860 *
861 * @param bool $is_pro Indicates if the product is a pro version.
862 *
863 * @return bool True if any scan is active, false otherwise.
864 */
865 private function is_any_active( bool $is_pro ): bool {
866 $settings = new Scan_Settings();
867 $file_change_check = $settings->is_checked_any_file_change_types();
868
869 if ( $is_pro ) {
870 // Pro version. Check all parent types.
871 return $file_change_check || $settings->check_known_vuln || $settings->scan_malware;
872 } else {
873 // Free version:
874 // Check the 'File change detection' type because only it's available with nested types.
875 // Check the Abandoned plugin type.
876 return $file_change_check || $settings->check_abandoned_plugin;
877 }
878 }
879
880 /**
881 * Exports strings.
882 *
883 * @return array An array of strings.
884 */
885 public function export_strings(): array {
886 $strings = array();
887 if ( $this->is_any_active( $this->is_pro ) ) {
888 $strings[] = esc_html__( 'Active', 'defender-security' );
889 } else {
890 $strings[] = esc_html__( 'Inactive', 'defender-security' );
891 }
892
893 $scan_report = new Malware_Report();
894 $scan_notification = new Malware_Notification();
895 if ( 'enabled' === $scan_notification->status ) {
896 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
897 }
898 if ( $this->is_pro && 'enabled' === $scan_report->status ) {
899 $strings[] = sprintf(
900 /* translators: %s: Frequency value. */
901 esc_html__( 'Email reports sending %s', 'defender-security' ),
902 $scan_report->frequency
903 );
904 } elseif ( ! $this->is_pro ) {
905 $strings[] = sprintf(
906 /* translators: %s: Html for Pro-tag. */
907 esc_html__( 'Email report inactive %s', 'defender-security' ),
908 '<span class="sui-tag sui-tag-pro">Pro</span>'
909 );
910 }
911
912 return $strings;
913 }
914
915 /**
916 * Generates configuration strings based on the provided configuration and
917 * whether the product is a pro version.
918 *
919 * @param array $config Configuration data.
920 * @param bool $is_pro Indicates if the product is a pro version.
921 *
922 * @return array Returns an array of configuration strings.
923 */
924 public function config_strings( array $config, bool $is_pro ): array {
925 $strings = array();
926 $strings[] = $this->service->is_any_scan_active( $config )
927 ? esc_html__( 'Active', 'defender-security' )
928 : esc_html__( 'Inactive', 'defender-security' );
929
930 if ( 'enabled' === $config['notification'] ) {
931 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
932 }
933 if ( $is_pro && 'enabled' === $config['report'] ) {
934 $strings[] = sprintf(
935 /* translators: %s: Frequency value. */
936 esc_html__( 'Email reports sending %s', 'defender-security' ),
937 $config['frequency']
938 );
939 } elseif ( ! $is_pro ) {
940 $strings[] = sprintf(
941 /* translators: %s: Html for Pro-tag. */
942 esc_html__( 'Email report inactive %s', 'defender-security' ),
943 '<span class="sui-tag sui-tag-pro">Pro</span>'
944 );
945 }
946
947 return $strings;
948 }
949
950 /**
951 * Run different scan actions based on the scan location.
952 *
953 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
954 *
955 * @return void
956 */
957 public function run_scan_mechanisms_from( $type ) {
958 $this->service->gather_actioned_plugin_details();
959 $this->do_async_scan( $type );
960 }
961
962 /**
963 * Triggers the asynchronous scan.
964 *
965 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
966 *
967 * @return void
968 */
969 public function do_async_scan( string $type ): void {
970 wd_di()->get( Model_Scan::class )->delete_idle();
971 // Delete the slug from the previous scan.
972 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
973
974 if ( function_exists( 'as_enqueue_async_action' ) ) {
975 as_enqueue_async_action(
976 'defender/async_scan',
977 array(
978 'type' => $type,
979 ),
980 'defender'
981 );
982 }
983 }
984
985 /**
986 * Clear completed action scheduler logs.
987 *
988 * @return void
989 * @since 2.6.5
990 */
991 public function clear_scan_logs(): void {
992 $scan_component = wd_di()->get( Scan_Component::class );
993 $result = $scan_component::clear_logs();
994
995 if ( isset( $result['error'] ) ) {
996 $this->log( 'WP CRON Error : ' . $result['error'], self::SCAN_LOG );
997 }
998 }
999
1000 /**
1001 * When user session is expired and scan is running, then don't login via heartbeat modal.
1002 *
1003 * @param array $response The no-priv Heartbeat response.
1004 * @param string $screen_id The screen id.
1005 *
1006 * @return mixed
1007 * @since 3.11.0
1008 */
1009 public function nopriv_heartbeat( $response, $screen_id ) {
1010 if ( false !== strpos( $screen_id, $this->slug ) ) {
1011 $scan = Model_Scan::get_active();
1012
1013 if ( is_object( $scan ) ) {
1014 $response['wp-auth-check'] = true;
1015 }
1016 }
1017
1018 return $response;
1019 }
1020
1021 /**
1022 * Triggers and send analytics data on scan completed.
1023 *
1024 * @param int $action_id Action ID.
1025 *
1026 * @return void
1027 */
1028 public function scan_completed_analytics( $action_id ) {
1029 if (
1030 class_exists( ActionScheduler::class )
1031 && method_exists( ActionScheduler::class, 'store' )
1032 && 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group()
1033 ) {
1034 $scan_analytics = wd_di()->get( Scan_Analytics::class );
1035
1036 $scan_model = wd_di()->get( Model_Scan::class );
1037 $analytics_data = $scan_analytics->scan_completed( $scan_model );
1038 if ( array() === $analytics_data ) {
1039 return;
1040 }
1041
1042 $this->track_feature(
1043 $analytics_data['event'],
1044 $analytics_data['data']
1045 );
1046 }
1047 }
1048 }
1049