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

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