PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/controller/class-scan.php +368 -304 5.3.1 → 6.3.0 View file →
@@ -8,14 +8,12 @@
8 8 namespace WP_Defender\Controller;
9 9
10 10 use ActionScheduler;
11 11 use WP_Defender\Event;
12 -use WP_Defender\Admin;
13 12 use Valitron\Validator;
14 13 use Calotes\Component\Request;
15 14 use Calotes\Component\Response;
16 15 use WP_Defender\Controller\Quarantine;
17 -use WP_Defender\Component\Rate;
18 16 use WP_Defender\Traits\Formats;
19 17 use WP_Defender\Traits\Scan_Upsell;
20 18 use WP_Defender\Model\Scan_Item;
21 19 use WP_Defender\Behavior\WPMUDEV;
@@ -20,9 +18,11 @@
20 18 use WP_Defender\Model\Scan_Item;
21 19 use WP_Defender\Behavior\WPMUDEV;
22 20 use WP_Defender\Model\Scan as Model_Scan;
23 21 use WP_Defender\Behavior\Scan\Core_Integrity;
22 +use WP_Defender\Component\Network_Cron_Manager;
24 23 use WP_Defender\Component\Scan as Scan_Component;
24 +use WP_Defender\Component\Rate as Rate_Component;
25 25 use WP_Defender\Model\Setting\Scan as Scan_Settings;
26 26 use WP_Defender\Model\Notification\Malware_Report;
27 27 use WP_Defender\Component\Config\Config_Hub_Helper;
28 28 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
@@ -27,8 +27,9 @@
27 27 use WP_Defender\Component\Config\Config_Hub_Helper;
28 28 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
29 29 use WP_Defender\Model\Notification\Malware_Notification;
30 30 use WP_Defender\Component\Quarantine as Quarantine_Component;
31 +use WP_Defender\Behavior\Scan\Plugin_Integrity;
31 32
32 33 /**
33 34 * Contains methods for handling scans.
34 35 */
@@ -37,9 +38,22 @@
37 38 use Formats;
38 39 use Scan_Upsell;
39 40
40 41 public const SCAN_LOG = 'scan.log';
42 +
41 43 /**
44 + * Records whether a scan has been started on this installation.
45 + *
46 + * @var string
47 + */
48 + public const FIRST_SCAN_STARTED = 'wp_defender_first_scan_started';
49 +
50 + /**
51 + * Default number of issue items per page on the Scan Issues UI.
52 + */
53 + public const DEFAULT_PER_PAGE = 10;
54 +
55 + /**
42 56 * The slug identifier for this controller.
43 57 *
44 58 * @var string
45 59 */
@@ -57,9 +71,8 @@
57 71 *
58 72 * @var Scan_Component
59 73 */
60 74 protected $service;
61 -
62 75 /**
63 76 * Quarantine controller.
64 77 *
65 78 * @var Quarantine
@@ -66,13 +79,13 @@
66 79 */
67 80 private $quarantine_controller;
68 81
69 82 /**
70 - * Indicates whether the current installation is a pro version.
83 + * Is the Malware Report option unlocked?
71 84 *
72 85 * @var bool
73 86 */
74 - private $is_pro;
87 + private $is_unlocked;
75 88
76 89 /**
77 90 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
78 91 */
@@ -77,24 +90,19 @@
77 90 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
78 91 */
79 92 public function __construct() {
80 93 $this->register_page(
81 - esc_html__( 'Malware Scanning', 'defender-security' ),
94 + $this->get_title(),
82 95 $this->slug,
83 - array(
84 - $this,
85 - 'main_view',
86 - ),
96 + array( $this, 'main_view' ),
87 97 $this->parent_slug
88 98 );
89 99
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();
100 + $this->model = wd_di()->get( Scan_Settings::class );
101 + $this->service = wd_di()->get( Scan_Component::class );
102 + $this->quarantine_controller = wd_di()->get( Quarantine::class );
93 103
94 - if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
95 - $this->quarantine_controller = wd_di()->get( Quarantine::class );
96 - }
104 + $this->is_unlocked = wd_di()->get( WPMUDEV::class )->is_pro();
97 105
98 106 $this->register_routes();
99 107 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
100 108 add_action( 'wp_ajax_defender_process_scan', array( $this, 'process' ) );
@@ -108,18 +116,24 @@
108 116 if (
109 117 is_admin() &&
110 118 'plugins.php' === $pagenow &&
111 119 apply_filters( 'wd_display_vulnerability_warnings', true ) &&
112 - $this->is_pro
120 + wd_di()->get( WPMUDEV::class )->is_apikey_available()
113 121 ) {
114 122 $this->service->display_vulnerability_warnings();
115 123 }
116 124
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' ) );
125 + /**
126 + * Schedule a time to clear completed action scheduler logs.
127 + *
128 + * @var Network_Cron_Manager $network_cron_manager
129 + */
130 + $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
131 + $network_cron_manager->register_callback(
132 + 'wpdef_clear_scan_logs',
133 + array( $this, 'clear_scan_logs' ),
134 + WEEK_IN_SECONDS
135 + );
122 136
123 137 add_filter( 'heartbeat_nopriv_send', array( $this, 'nopriv_heartbeat' ), 10, 2 );
124 138
125 139 add_action(
@@ -128,8 +142,17 @@
128 142 );
129 143 }
130 144
131 145 /**
146 + * Return the title of the page.
147 + *
148 + * @return string The title of the page.
149 + */
150 + public function get_title(): string {
151 + return esc_html__( 'Issues', 'defender-security' );
152 + }
153 +
154 + /**
132 155 * Clean up data after core updating.
133 156 *
134 157 * @return void
135 158 */
@@ -139,19 +162,35 @@
139 162
140 163 /**
141 164 * Start a scan.
142 165 *
166 + * @param Request $request Request object.
167 + *
143 168 * @return Response
144 169 * @defender_route
145 170 * @defender_redirect
146 171 */
147 - public function start(): Response {
172 + public function start( Request $request ): Response {
173 + $data = $request->get_data(
174 + array(
175 + 'scan_type' => array(
176 + 'type' => 'string',
177 + 'sanitize' => 'sanitize_key',
178 + ),
179 + )
180 + );
181 + $scan_type = in_array( $data['scan_type'] ?? '', array( 'deep', 'malware' ), true )
182 + ? $data['scan_type']
183 + : 'malware';
184 +
148 185 $model = Model_Scan::create();
149 186 if ( is_object( $model ) && ! is_wp_error( $model ) ) {
187 + update_site_option( self::FIRST_SCAN_STARTED, '1' );
188 + set_transient( 'defender_scan_triggered_by_' . $model->id, get_current_user_id(), HOUR_IN_SECONDS );
189 + Model_Scan::set_scan_type( $scan_type );
150 190 $this->log( 'Initial ping self', self::SCAN_LOG );
191 + $this->run_scan_mechanisms_from( 'scan' );
151 192
152 - $this->do_async_scan( 'scan' );
153 -
154 193 return new Response(
155 194 true,
156 195 array(
157 196 'status' => $model->status,
@@ -156,8 +195,9 @@
156 195 array(
157 196 'status' => $model->status,
158 197 'status_text' => $model->get_status_text(),
159 198 'percent' => 0,
199 + 'scan_type' => $scan_type,
160 200 )
161 201 );
162 202 }
163 203
@@ -176,16 +216,15 @@
176 216 * @defender_route
177 217 * @is_public
178 218 */
179 219 public function process() {
180 - if ( $this->service->has_lock() ) {
220 + $lock_filename = $this->service->get_lock_filename();
221 + if ( ! $this->service->try_create_lock( $lock_filename ) ) {
181 222 $this->log( 'Fallback as already a process is running', self::SCAN_LOG );
182 223
183 224 return;
184 225 }
185 226
186 - // This creates file lock, for make sure only 1 process run as a time.
187 - $this->service->create_lock();
188 227 // Check if the ping is from self or not.
189 228 $ret = $this->service->process();
190 229 $this->log( 'process done, queue for next', self::SCAN_LOG );
191 230 if ( false === $ret ) {
@@ -190,13 +229,13 @@
190 229 $this->log( 'process done, queue for next', self::SCAN_LOG );
191 230 if ( false === $ret ) {
192 231 // Ping self.
193 232 $this->log( 'Scan not done, pinging', self::SCAN_LOG );
194 - $this->service->remove_lock();
233 + $this->service->remove_lock( $lock_filename );
195 234 $this->process();
196 235 } else {
197 236 $this->queue_to_sync_with_hub();
198 - $this->service->remove_lock();
237 + $this->service->remove_lock( $lock_filename );
199 238 }
200 239 }
201 240
202 241 /**
@@ -206,14 +245,16 @@
206 245 * @defender_route
207 246 * @defender_redirect
208 247 */
209 248 public function status(): Response {
249 + $scan_type = Model_Scan::get_scan_type();
210 250 $idle_scan = wd_di()->get( Model_Scan::class )->get_idle();
211 251
212 252 if ( is_object( $idle_scan ) ) {
213 253 $this->service->update_idle_scan_status();
254 + $response = $this->get_status_response_data( $idle_scan, $scan_type );
214 255
215 - return new Response( false, $idle_scan->to_array() );
256 + return new Response( true, $response );
216 257 }
217 258
218 259 $checksum_issue = get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 'false' );
219 260 $checksum_scan = Model_Scan::get_core_check();
@@ -218,21 +259,34 @@
218 259 $checksum_issue = get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 'false' );
219 260 $checksum_scan = Model_Scan::get_core_check();
220 261 if ( 'false' !== $checksum_issue && is_object( $checksum_scan ) ) {
221 262 $this->service->update_idle_scan_status_by_checksum_issue( $checksum_scan );
263 + $response = $this->get_status_response_data( $checksum_scan, $scan_type );
222 264
223 - return new Response( false, $checksum_scan->to_array() );
265 + return new Response( true, $response );
224 266 }
225 267
226 268 $scan = Model_Scan::get_active();
227 269 if ( is_object( $scan ) ) {
270 + $response = $this->get_status_response_data( $scan, $scan_type );
228 271
229 - return new Response( false, $scan->to_array() );
272 + return new Response( true, $response );
230 273 }
274 +
231 275 $scan = Model_Scan::get_last();
232 276 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
277 + $response = array_merge( $this->get_status_response_data( $scan, $scan_type ), $this->get_last_scan_time_data( $scan ) );
278 + $response['message'] = __( 'Malware scan completed successfully!', 'defender-security' );
279 + $response['scan_type'] = $scan_type;
280 + if ( 'deep' === $scan_type ) {
281 + $security_tweaks = wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->dashboard_widget();
282 + $response['hardening_count'] = (int) ( $security_tweaks['summary']['issues_count'] ?? 0 );
283 + }
284 + if ( isset( $this->quarantine_controller ) ) {
285 + $response['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array();
286 + }
233 287
234 - return new Response( true, $scan->to_array() );
288 + return new Response( true, $response );
235 289 }
236 290
237 291 return new Response(
238 292 false,
@@ -242,8 +296,24 @@
242 296 );
243 297 }
244 298
245 299 /**
300 + * Build the compatible status payload and consume its queued messages.
301 + *
302 + * @param Model_Scan $scan Scan model used to build the status payload.
303 + * @param string $scan_type Current scan type.
304 + */
305 + private function get_status_response_data( Model_Scan $scan, string $scan_type ): array {
306 + $response = $scan->to_array();
307 + $response['status_text'] = $response['status_text'] ?? $scan->get_status_text();
308 + $response['percent'] = $response['percent'] ?? $scan->percent;
309 + $response['scan_type'] = $scan_type;
310 + $response['status_messages'] = $scan->drain_status_messages();
311 +
312 + return $response;
313 + }
314 +
315 + /**
246 316 * Cancel current scan.
247 317 *
248 318 * @return Response
249 319 * @defender_route
@@ -251,8 +321,9 @@
251 321 */
252 322 public function cancel(): Response {
253 323 $component = wd_di()->get( Scan_Component::class );
254 324 $component->cancel_a_scan();
325 + Model_Scan::clear_scan_type();
255 326 $last = Model_Scan::get_last();
256 327 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
257 328 $last = $last->to_array();
258 329 }
@@ -308,8 +379,16 @@
308 379 $resolution_method = 'Update';
309 380 }
310 381 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $scan_item->type ) {
311 382 $threat_type = 'Suspicious function';
383 + } elseif (
384 + in_array(
385 + $scan_item->type,
386 + Model_Scan::get_abandoned_types(),
387 + true
388 + )
389 + ) {
390 + $threat_type = 'Outdated & removed plugins';
312 391 }
313 392
314 393 $this->track_feature(
315 394 $event_name,
@@ -355,9 +434,9 @@
355 434 $intention,
356 435 $allowed_intentions,
357 436 true
358 437 ) ) {
359 - wp_die();
438 + return new Response( false, array() );
360 439 }
361 440
362 441 $scan = Model_Scan::get_last();
363 442 if ( $scan instanceof Model_Scan ) {
@@ -362,14 +441,22 @@
362 441 $scan = Model_Scan::get_last();
363 442 if ( $scan instanceof Model_Scan ) {
364 443 $item = $scan->get_issue( $id );
365 444 if ( is_object( $item ) && $item->has_method( $intention ) ) {
445 + if ( 'resolve' === $intention ) {
446 + $result = $item->resolve();
447 + } elseif ( 'quarantine' === $intention ) {
448 + $result = $item->quarantine( $data['parent_action'], $item->owner );
449 + } elseif ( 'delete' === $intention ) {
450 + $result = $item->delete();
451 + } elseif ( 'ignore' === $intention ) {
452 + $result = $item->ignore();
453 + } elseif ( 'unignore' === $intention ) {
454 + $result = $item->unignore();
455 + } elseif ( 'pull_src' === $intention ) {
456 + $result = $item->pull_src();
457 + }
366 458
367 - if ( 'quarantine' === $intention ) {
368 - $result = $item->$intention( $data['parent_action'] );
369 - } else {
370 - $result = $item->$intention();
371 - }
372 459 // Maybe track.
373 460 if ( $this->is_tracking_active() ) {
374 461 $this->item_action_analytics( $item, $intention );
375 462 }
@@ -401,8 +488,12 @@
401 488
402 489 if ( $scan instanceof Model_Scan ) {
403 490 $result['scan'] = $scan->to_array();
404 491
492 + if ( 'quarantine' === $intention && isset( $this->quarantine_controller ) ) {
493 + $result['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array();
494 + }
495 +
405 496 $success = true;
406 497 if ( isset( $result['success'] ) && false === $result['success'] ) {
407 498 $success = false;
408 499 }
@@ -440,10 +531,10 @@
440 531 $items = $data['items'] ?? array();
441 532 $intention = $data['bulk'] ?? false;
442 533
443 534 if (
444 - empty( $items )
445 - || ! is_array( $items )
535 + ! is_array( $items )
536 + || array() === $items
446 537 || ! in_array( $intention, array( 'ignore', 'unignore', 'delete' ), true )
447 538 ) {
448 539 return new Response( false, array() );
449 540 }
@@ -458,13 +549,11 @@
458 549 $none_delete_items = array();
459 550 $sync_hub = false;
460 551 foreach ( $items as $id ) {
461 552 if ( 'ignore' === $intention ) {
462 - $scan->ignore_issue( (int) $id );
463 - $sync_hub = true;
553 + $sync_hub = $scan->ignore_issue( (int) $id );
464 554 } elseif ( 'unignore' === $intention ) {
465 - $scan->unignore_issue( (int) $id );
466 - $sync_hub = true;
555 + $sync_hub = $scan->unignore_issue( (int) $id );
467 556 } elseif ( 'delete' === $intention ) {
468 557 $item = $scan->get_issue( (int) $id );
469 558 // Work with every item.
470 559 if ( is_object( $item ) && $item->has_method( $intention ) ) {
@@ -487,9 +576,9 @@
487 576 $this->queue_to_sync_with_hub();
488 577 }
489 578
490 579 $result = array();
491 - if ( ! empty( $none_delete_items ) ) {
580 + if ( array() !== $none_delete_items ) {
492 581 $result['message'] = sprintf(
493 582 /* translators: %s: Vulnerability item(es) */
494 583 _n(
495 584 'Defender doesn\'t have enough permission to remove this file: %s',
@@ -509,9 +598,9 @@
509 598 // Refresh scan instance.
510 599 $scan = Model_Scan::get_last();
511 600 $result['scan'] = $scan->to_array();
512 601
513 - return new Response( empty( $none_delete_items ), $result );
602 + return new Response( array() === $none_delete_items, $result );
514 603 }
515 604
516 605 /**
517 606 * Save settings.
@@ -524,18 +613,15 @@
524 613 * @defender_route
525 614 */
526 615 public function save_settings( Request $request ): Response {
527 616 $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;
617 + // Prepare for the state's change.
618 + $old_integrity_check_state = $this->model->integrity_check;
619 + // Case#1: inherit the parent's state to nested options.
620 + if ( $old_integrity_check_state !== $data['integrity_check'] ) {
621 + $data['check_core'] = $data['integrity_check'];
622 + $data['check_plugins'] = $data['integrity_check'];
536 623 }
537 -
538 624 // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice.
539 625 if ( $data['scan_malware'] && ! $data['integrity_check'] ) {
540 626 $response = array(
541 627 'type_notice' => 'info',
@@ -551,36 +637,17 @@
551 637 '</strong>'
552 638 ),
553 639 );
554 640 } else {
555 - // Prepare response message for usual successful case.
556 641 $response = array(
557 642 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
558 643 'auto_close' => true,
559 644 );
560 645 }
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 646 $before_import_schedule = $this->model->quarantine_expire_schedule;
579 647
580 648 $this->model->import( $data );
581 649 if ( $this->model->validate() ) {
582 -
583 650 if ( class_exists( 'WP_Defender\Component\Quarantine' ) ) {
584 651 $quarantine_component = wd_di()->get( Quarantine_Component::class );
585 652 $quarantine_component->reschedule_file_expiry_cron(
586 653 $before_import_schedule,
@@ -587,14 +654,9 @@
587 654 $data['quarantine_expire_schedule']
588 655 );
589 656 }
590 657
591 - // Todo: need to disable Malware_Notification & Malware_Report if all scan settings are deactivated?
592 658 $this->model->save();
593 - // Save Report's changes.
594 - if ( $report_change ) {
595 - $report->save();
596 - }
597 659 Config_Hub_Helper::set_clear_active_flag();
598 660
599 661 return new Response(
600 662 true,
@@ -628,18 +690,18 @@
628 690 'type' => 'string',
629 691 'sanitize' => 'sanitize_text_field',
630 692 ),
631 693 'type' => array(
632 - 'type' => 'string',
694 + 'type' => 'array',
633 695 'sanitize' => 'sanitize_text_field',
634 696 ),
635 697 'per_page' => array(
636 698 'type' => 'string',
637 - 'sanitize' => 'sanitize_text_field',
699 + 'sanitize' => 'intval',
638 700 ),
639 701 'paged' => array(
640 702 'type' => 'int',
641 - 'sanitize' => 'sanitize_text_field',
703 + 'sanitize' => 'intval',
642 704 ),
643 705 )
644 706 );
645 707
@@ -649,74 +711,166 @@
649 711 if ( ! $v->validate() ) {
650 712 return new Response(
651 713 false,
652 714 array(
653 - 'message' => '',
715 + 'message' => esc_html__( 'Wrong scan issue data.', 'defender-security' ),
654 716 )
655 717 );
656 718 }
657 719
658 720 $scan = Model_Scan::get_last();
659 - $issues = $scan->to_array( $data['per_page'], $data['paged'], $data['type'] );
721 + $issues = $scan->to_array( $data['per_page'], $data['paged'], $this->normalize_issue_types( $data['type'] ), $data['scenario'] );
660 722
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 - )
723 + $response = array(
724 + 'issue' => $issues['issues_items'],
725 + 'ignored' => $issues['ignored_items'],
726 + 'paging' => $issues['paging'],
727 + 'count' => $issues['count'],
669 728 );
729 +
730 + if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
731 + $response['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array();
732 + }
733 +
734 + return new Response( true, $response );
670 735 }
671 736
672 737 /**
673 - * Handle notice.
674 - * Send the notice to the admin dashboard of the site.
738 + * Normalize issue type filters from the legacy and redesign payloads.
675 739 *
676 - * @param Request $request Request object.
740 + * @param array|string $types Requested issue type(s).
677 741 *
678 - * @return Response Response object.
679 - * @defender_route
742 + * @return array|string
680 743 */
681 - public function handle_notice( Request $request ): Response {
682 - update_site_option( Rate::SLUG_FOR_BUTTON_RATE, true );
744 + private function normalize_issue_types( $types ) {
745 + $type_aliases = array(
746 + 'core' => Scan_Item::TYPE_INTEGRITY,
747 + 'plugin' => Scan_Item::TYPE_PLUGIN_CHECK,
748 + 'suspicious' => Scan_Item::TYPE_SUSPICIOUS,
749 + 'known_vulnerability' => Scan_Item::TYPE_VULNERABILITY,
750 + 'plugin_closed' => Scan_Item::TYPE_PLUGIN_CLOSED,
751 + 'plugin_outdated' => Scan_Item::TYPE_PLUGIN_OUTDATED,
752 + );
683 753
684 - return new Response( true, array() );
754 + $types = is_array( $types ) ? $types : array( $types );
755 + $types = array_filter(
756 + array_map(
757 + static function ( $type ) use ( $type_aliases ) {
758 + return $type_aliases[ $type ] ?? $type;
759 + },
760 + $types
761 + ),
762 + 'boolval'
763 + );
764 + $types = array_values( array_unique( $types ) );
765 +
766 + if ( 1 === count( $types ) ) {
767 + return $types[0];
768 + }
769 +
770 + return $types;
685 771 }
686 772
687 773 /**
688 - * Handle postponed notice.
689 - * Reset counters for postponed notice.
774 + * Get relative and exact display times for the last completed scan.
690 775 *
691 - * @param Request $request Request object.
776 + * @param Model_Scan|null $last Last completed scan.
692 777 *
693 - * @return Response Response object.
694 - * @defender_route
778 + * @return array
695 779 */
696 - public function postpone_notice( Request $request ): Response {
697 - Rate::reset_counters();
780 + private function get_last_scan_time_data( $last ): array {
781 + if ( ! is_object( $last ) || ! isset( $last->date_start ) || '' === $last->date_start ) {
782 + return array(
783 + 'last_scan' => '',
784 + 'last_scan_time' => '',
785 + );
786 + }
698 787
699 - return new Response( true, array() );
788 + $last_scan_timestamp = strtotime( $last->date_start . ' UTC' );
789 + $time_difference = time() - $last_scan_timestamp;
790 + $data = array(
791 + 'last_scan' => sprintf(
792 + /* translators: %s: human-readable time difference, e.g. "5 minutes ago" */
793 + __( 'Scanned %s ago', 'defender-security' ),
794 + human_time_diff( $last_scan_timestamp )
795 + ),
796 + );
797 +
798 + if ( $time_difference < DAY_IN_SECONDS ) {
799 + $data['last_scan_time'] = wp_date( 'g:i A', $last_scan_timestamp );
800 + } elseif ( $time_difference < YEAR_IN_SECONDS ) {
801 + $data['last_scan_time'] = wp_date( 'D, j M g:i A', $last_scan_timestamp );
802 + } else {
803 + $data['last_scan_time'] = wp_date( 'j M Y, g:i A', $last_scan_timestamp );
804 + }
805 +
806 + return $data;
700 807 }
701 808
702 809 /**
703 - * Handle refuse notice.
704 - * Send the refuse notice to the admin dashboard of the site.
810 + * Returns scan result data for the frontend on page load.
705 811 *
706 - * @param Request $request Request object.
707 - *
708 - * @return Response Response object.
709 - * @defender_route
812 + * @return array
710 813 */
711 - public function refuse_notice( Request $request ): Response {
712 - update_site_option( Rate::SLUG_FOR_BUTTON_THANKS, true );
814 + public function get_initial_scan_data(): array {
815 + $scan = Model_Scan::get_active();
816 + $last = Model_Scan::get_last();
817 + $per_page = self::DEFAULT_PER_PAGE;
818 + $paged = 1;
713 819
714 - return new Response( true, array() );
820 + if ( ! is_object( $scan ) && ! is_object( $last ) ) {
821 + $scan_data = null;
822 + } elseif ( is_object( $scan ) && is_object( $last ) ) {
823 + // If an active scan exists AND there's a previous completed scan,
824 + // merge the active scan's progress with the last scan's issue data.
825 + // This ensures that during a page refresh while scanning, users still
826 + // see the previous scan results while the new scan is in progress.
827 + $scan_data = $scan->to_array( $per_page, $paged );
828 + $last_data = $last->to_array( $per_page, $paged );
829 + // Preserve previous scan's issue data.
830 + $scan_data['issues_items'] = $last_data['issues_items'] ?? array();
831 + $scan_data['ignored_items'] = $last_data['ignored_items'] ?? array();
832 + $scan_data['count'] = $last_data['count'] ?? array();
833 + $scan_data['paging'] = $last_data['paging'] ?? array();
834 + } elseif ( is_object( $scan ) ) {
835 + $scan_data = $scan->to_array( $per_page, $paged );
836 + } else {
837 + $scan_data = $last->to_array( $per_page, $paged );
838 + }
839 +
840 + $first_scan_started = get_site_option( self::FIRST_SCAN_STARTED, null );
841 + if ( null === $first_scan_started ) {
842 + // Existing installations backward compatibility.
843 + $first_scan_started = is_object( $scan ) || is_object( $last );
844 + } else {
845 + $first_scan_started = '1' === (string) $first_scan_started;
846 + }
847 +
848 + $data = array(
849 + 'scan' => $scan_data,
850 + 'has_first_scan_started' => $first_scan_started,
851 + 'isEnabledScanType' => $this->service->is_any_scan_type_active(),
852 + );
853 +
854 + // Always expose the last completed scan time at the outer level so the
855 + // dashboard can show it even while a new scan is actively running.
856 + $data = array_merge( $data, $this->get_last_scan_time_data( $last ) );
857 +
858 + if ( isset( $this->quarantine_controller ) ) {
859 + $data['quarantine'] = array(
860 + 'list' => $this->quarantine_controller->data_frontend()['list'] ?? array(),
861 + );
862 + }
863 + // Display Rate notice. Without the rating's type.
864 + $data['isRatingDisplayed'] = defender_is_wp_org_version()
865 + && is_array( $scan_data ) && isset( $data['scan']['issues_items'] )
866 + && Rate_Component::is_displayed_in_redesigned_version( count( $data['scan']['issues_items'] ) );
867 +
868 + return array( 'scan' => $data );
715 869 }
716 870
717 871 /**
718 - * Render main page.
872 + * Render the main page.
719 873 *
720 874 * @return void
721 875 */
722 876 public function main_view(): void {
@@ -730,94 +884,59 @@
730 884 public function enqueue_assets() {
731 885 if ( ! $this->is_page_active() ) {
732 886 return;
733 887 }
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 );
888 +
889 + $handle = 'defender-ui-scan';
740 890 wp_enqueue_script(
741 - 'def-codemirror-clike',
742 - defender_asset_url( '/assets/js/vendor/codemirror/clike/clike.js' ),
743 - array( 'def-codemirror' ),
891 + $handle,
892 + WP_DEFENDER_BASE_URL . 'assets/js/scan-ui.js',
893 + array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
744 894 DEFENDER_VERSION,
745 895 true
746 896 );
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
897 + wp_set_script_translations( $handle, 'wpdef' );
898 +
899 + $scan_routes_data = $this->dump_routes_and_nonces();
900 + $quarantine_routes_data = $this->quarantine_controller->dump_routes_and_nonces();
901 + if ( defender_is_wp_org_version() ) {
902 + $rate_routes_nonces = wd_di()->get( \WP_Defender\Controller\Rate::class )->dump_routes_and_nonces();
903 + $rate_routes = $rate_routes_nonces['routes'];
904 + $rate_nonces = $rate_routes_nonces['nonces'];
905 + } else {
906 + $rate_routes = array();
907 + $rate_nonces = array();
908 + }
909 +
910 + wp_localize_script(
911 + $handle,
912 + 'defenderUIData',
913 + array_merge(
914 + $this->get_shared_data(),
915 + array(
916 + 'routes' => array_merge(
917 + $scan_routes_data['routes'],
918 + $quarantine_routes_data['routes'],
919 + $rate_routes
920 + ),
921 + 'nonces' => array_merge(
922 + $scan_routes_data['nonces'],
923 + $quarantine_routes_data['nonces'],
924 + $rate_nonces
925 + ),
926 + ),
927 + $this->get_initial_scan_data()
928 + )
754 929 );
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 930
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 931 wp_enqueue_style(
803 - 'def-codemirror-merge',
804 - defender_asset_url( '/assets/js/vendor/codemirror/merge/merge.css' ),
805 - array( 'def-codemirror' ),
932 + $handle,
933 + WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css',
934 + array(),
806 935 DEFENDER_VERSION
807 936 );
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 - );
937 +
938 + $this->enqueue_main_assets();
820 939 }
821 940
822 941 /**
823 942 * Converts the current object state to an array.
@@ -855,10 +974,15 @@
855 974 /**
856 975 * Delete all the data & the cache.
857 976 */
858 977 public function remove_data(): void {
978 + delete_site_option( self::FIRST_SCAN_STARTED );
859 979 delete_site_option( Model_Scan::IGNORE_INDEXER );
980 + delete_site_option( Model_Scan::OPTION_SCAN_TYPE );
981 + Model_Scan::clear_all_status_messages();
860 982 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
983 + delete_site_transient( Plugin_Integrity::$org_slugs );
984 + delete_site_transient( Plugin_Integrity::$org_responses );
861 985 }
862 986
863 987 /**
864 988 * Provides data for the frontend.
@@ -874,68 +998,23 @@
874 998 $scan = null;
875 999 } else {
876 1000 $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged );
877 1001 }
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 - }
1002 + $settings = new Scan_Settings();
1003 + $report = wd_di()->get( Malware_Report::class );
914 1004
915 - // Todo: add logic for deactivated scan settings. Maybe display some notice.
916 - $data = array(
1005 + $scan['isEnabledScanType'] = $this->service->is_any_scan_type_active();
1006 +
1007 + $data = array(
917 1008 'scan' => $scan,
918 1009 '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 1010 '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' ),
1011 + 'misc' => array(
1012 + 'labels' => $settings->labels(),
931 1013 ),
932 1014 );
1015 + $data['quarantine'] = $this->quarantine_controller->data_frontend();
933 1016
934 - if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) {
935 - $data['quarantine'] = $this->quarantine_controller->data_frontend();
936 - }
937 -
938 1017 return array_merge( $data, $this->dump_routes_and_nonces() );
939 1018 }
940 1019
941 1020 /**
@@ -944,12 +1023,12 @@
944 1023 * @param array $data Data to be imported into the model.
945 1024 */
946 1025 public function import_data( array $data ) {
947 1026 $model = $this->model;
948 - if ( empty( $data ) ) {
1027 + if ( array() === $data ) {
949 1028 $model->scheduled_scanning = false;
950 1029 $model->frequency = 'weekly';
951 - $model->day_n = '1';
1030 + $model->day_n = 1;
952 1031 $model->day = 'sunday';
953 1032 $model->time = '4:00';
954 1033 $model->save();
955 1034 } else {
@@ -960,28 +1039,8 @@
960 1039 }
961 1040 }
962 1041
963 1042 /**
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 1043 * Exports strings.
985 1044 *
986 1045 * @return array An array of strings.
987 1046 */
@@ -986,48 +1045,37 @@
986 1045 * @return array An array of strings.
987 1046 */
988 1047 public function export_strings(): array {
989 1048 $strings = array();
990 - if ( $this->is_any_active( $this->is_pro ) ) {
1049 + if ( $this->service->is_any_scan_type_active() ) {
991 1050 $strings[] = esc_html__( 'Active', 'defender-security' );
992 1051 } else {
993 1052 $strings[] = esc_html__( 'Inactive', 'defender-security' );
994 1053 }
995 1054
996 - $scan_report = new Malware_Report();
997 1055 $scan_notification = new Malware_Notification();
998 1056 if ( 'enabled' === $scan_notification->status ) {
999 1057 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1000 1058 }
1001 - if ( $this->is_pro && 'enabled' === $scan_report->status ) {
1002 1059 $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 1060 /* translators: %s: Html for Pro-tag. */
1010 - esc_html__( 'Email report inactive %s', 'defender-security' ),
1061 + esc_html__( 'Scheduled scan inactive %s', 'defender-security' ),
1011 1062 '<span class="sui-tag sui-tag-pro">Pro</span>'
1012 1063 );
1013 - }
1014 1064
1015 1065 return $strings;
1016 1066 }
1017 1067
1018 1068 /**
1019 - * Generates configuration strings based on the provided configuration and
1020 - * whether the product is a pro version.
1069 + * Generates configuration strings based on the provided configuration.
1021 1070 *
1022 1071 * @param array $config Configuration data.
1023 - * @param bool $is_pro Indicates if the product is a pro version.
1024 1072 *
1025 1073 * @return array Returns an array of configuration strings.
1026 1074 */
1027 - public function config_strings( array $config, bool $is_pro ): array {
1075 + public function config_strings( array $config ): array {
1028 1076 $strings = array();
1029 - $strings[] = $this->service->is_any_scan_active( $config, $is_pro )
1077 + $strings[] = $this->service->check_scan_active_by( $config )
1030 1078 ? esc_html__( 'Active', 'defender-security' )
1031 1079 : esc_html__( 'Inactive', 'defender-security' );
1032 1080
1033 1081 if ( 'enabled' === $config['notification'] ) {
@@ -1032,18 +1080,13 @@
1032 1080
1033 1081 if ( 'enabled' === $config['notification'] ) {
1034 1082 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1035 1083 }
1036 - if ( $is_pro && 'enabled' === $config['report'] ) {
1084 + // No Pro membership and No hosted site.
1085 + if ( ! $this->is_unlocked ) {
1037 1086 $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 1087 /* translators: %s: Html for Pro-tag. */
1045 - esc_html__( 'Email report inactive %s', 'defender-security' ),
1088 + esc_html__( 'Scheduled scan inactive %s', 'defender-security' ),
1046 1089 '<span class="sui-tag sui-tag-pro">Pro</span>'
1047 1090 );
1048 1091 }
1049 1092
@@ -1050,8 +1093,20 @@
1050 1093 return $strings;
1051 1094 }
1052 1095
1053 1096 /**
1097 + * Run different scan actions based on the scan location.
1098 + *
1099 + * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
1100 + *
1101 + * @return void
1102 + */
1103 + public function run_scan_mechanisms_from( $type ) {
1104 + $this->service->gather_actioned_plugin_details();
1105 + $this->do_async_scan( $type );
1106 + }
1107 +
1108 + /**
1054 1109 * Triggers the asynchronous scan.
1055 1110 *
1056 1111 * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report.
1057 1112 *
@@ -1061,15 +1116,17 @@
1061 1116 wd_di()->get( Model_Scan::class )->delete_idle();
1062 1117 // Delete the slug from the previous scan.
1063 1118 delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
1064 1119
1065 - as_enqueue_async_action(
1066 - 'defender/async_scan',
1067 - array(
1068 - 'type' => $type,
1069 - ),
1070 - 'defender'
1071 - );
1120 + if ( function_exists( 'as_enqueue_async_action' ) ) {
1121 + as_enqueue_async_action(
1122 + 'defender/async_scan',
1123 + array(
1124 + 'type' => $type,
1125 + ),
1126 + 'defender'
1127 + );
1128 + }
1072 1129 }
1073 1130
1074 1131 /**
1075 1132 * Clear completed action scheduler logs.
@@ -1114,13 +1171,20 @@
1114 1171 *
1115 1172 * @return void
1116 1173 */
1117 1174 public function scan_completed_analytics( $action_id ) {
1118 - if ( 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group() ) {
1175 + if (
1176 + class_exists( ActionScheduler::class )
1177 + && method_exists( ActionScheduler::class, 'store' )
1178 + && 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group()
1179 + ) {
1119 1180 $scan_analytics = wd_di()->get( Scan_Analytics::class );
1120 1181
1121 1182 $scan_model = wd_di()->get( Model_Scan::class );
1122 1183 $analytics_data = $scan_analytics->scan_completed( $scan_model );
1184 + if ( array() === $analytics_data ) {
1185 + return;
1186 + }
1123 1187
1124 1188 $this->track_feature(
1125 1189 $analytics_data['event'],
1126 1190 $analytics_data['data']