PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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 / component / class-scan.php

class-scan.php in Defender Security – Malware Scanner, Login Security & Firewall 5.4.0, at src/component/class-scan.php

789 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handling the scanning process.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use Countable;
11 use ArrayIterator;
12 use WP_Defender\Admin;
13 use WP_Defender\Component;
14 use WP_Plugins_List_Table;
15 use WP_Defender\Model\Scan_Item;
16 use WP_Defender\Behavior\WPMUDEV;
17 use WP_Defender\Model\Scan as Scan_Model;
18 use WP_Defender\Behavior\Scan\Gather_Fact;
19 use WP_Defender\Behavior\Scan\Malware_Scan;
20 use WP_Defender\Behavior\Scan\Core_Integrity;
21 use WP_Defender\Behavior\Scan\Plugin_Integrity;
22 use WP_Defender\Behavior\Scan\Malware_Deep_Scan;
23 use WP_Defender\Behavior\Scan\Malware_Quick_Scan;
24 use WP_Defender\Behavior\Scan\Known_Vulnerability;
25 use WP_Defender\Model\Setting\Scan as Scan_Settings;
26 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
27 use WP_Defender\Controller\Scan as Scan_Controller;
28
29 /**
30 * The Scan class handles the scanning process, managing tasks, and coordinating different types of scans.
31 */
32 class Scan extends Component {
33
34 /**
35 * The current scan model.
36 *
37 * @var Scan_Model
38 */
39 public $scan;
40
41 /**
42 * Scan settings model.
43 *
44 * @var Scan_Settings
45 */
46 public $settings;
47
48 /**
49 * Details of vulnerabilities found during the scan.
50 *
51 * @var array
52 */
53 protected $vulnerability_details = array();
54
55 /**
56 * Instance of Known_Vulnerability to handle known vulnerability checks.
57 *
58 * @var Known_Vulnerability
59 */
60 private $known_vulnerability;
61
62 /**
63 * Instance of Malware_Scan to handle malware scanning.
64 *
65 * @var Malware_Scan
66 */
67 private $malware_scan;
68
69 /**
70 * Instance of Gather_Fact to gather necessary information before scanning.
71 *
72 * @var Gather_Fact|null
73 */
74 private ?Gather_Fact $gather_fact;
75
76 /**
77 * Lock file name for scanning.
78 *
79 * @var string
80 */
81 protected string $lock_filename = 'scan.lock';
82
83 /**
84 * Constructs the Scan object and initializes behaviors.
85 */
86 public function __construct() {
87 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
88 $this->attach_behavior( Core_Integrity::class, Core_Integrity::class );
89 $this->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
90 }
91
92 /**
93 * Performs additional actions after an advanced scan.
94 *
95 * @param object $model The scan model.
96 */
97 public function advanced_scan_actions( $model ) {
98 $this->reindex_ignored_issues( $model );
99 $this->clean_up();
100
101 if ( defender_is_wp_org_version() ) {
102 Rate::run_counter_of_completed_scans();
103 }
104 }
105
106 /**
107 * Process current scan.
108 *
109 * @return bool|int
110 */
111 public function process() {
112 $scan = Scan_Model::get_active();
113 if ( ! is_object( $scan ) ) {
114 // This case can be a scan get cancel.
115 return - 1;
116 }
117 $this->scan = $scan;
118 $tasks = $this->get_tasks();
119 $runner = new ArrayIterator( $tasks );
120 $task = $this->scan->status;
121 if ( Scan_Model::STATUS_INIT === $scan->status ) {
122 // Get the first.
123 $this->log( 'Prepare facts for a scan', Scan_Controller::SCAN_LOG );
124 $task = Scan_Model::STEP_GATHER_INFO;
125 $this->scan->percent = 0;
126 $this->scan->total_tasks = $runner->count();
127 $this->scan->save();
128 }
129 if (
130 in_array(
131 $this->scan->status,
132 array(
133 Scan_Model::STATUS_ERROR,
134 Scan_Model::STATUS_IDLE,
135 ),
136 true
137 )
138 ) {
139 // Stop and return true to abort the process.
140 return true;
141 }
142 // Find the current task.
143 $offset = array_search( $task, array_values( $tasks ), true );
144 if ( false === $offset ) {
145 $this->log( sprintf( 'offset is not found, search %s', $task ), Scan_Controller::SCAN_LOG );
146
147 return false;
148 }
149 // Reset the tasks to current.
150 $runner->seek( $offset );
151 $this->log( sprintf( 'Current task %s', $runner->current() ), Scan_Controller::SCAN_LOG );
152 if ( $this->has_method( $task ) ) {
153 $this->log( sprintf( 'processing %s', $runner->key() ), Scan_Controller::SCAN_LOG );
154 $result = $this->task_handler( $task );
155 if ( true === $result ) {
156 $this->log( sprintf( 'task %s processed', $runner->key() ), Scan_Controller::SCAN_LOG );
157 // Task is done, move to next.
158 $runner->next();
159 if ( $runner->valid() ) {
160 $this->log( sprintf( 'queue %s for next', $runner->key() ), Scan_Controller::SCAN_LOG );
161 $this->scan->status = $runner->key();
162 $this->scan->task_checkpoint = '';
163 $this->scan->date_end = gmdate( 'Y-m-d H:i:s' );
164 $this->scan->save();
165 // Queue for next run.
166 return false;
167 }
168 $this->log( 'All done!', Scan_Controller::SCAN_LOG );
169 // No more task in the queue, we are done.
170 $this->scan->status = Scan_Model::STATUS_FINISH;
171 $this->scan->save();
172 $this->advanced_scan_actions( $this->scan );
173 do_action( 'defender_notify', 'malware-notification', $this->scan );
174
175 return true;
176 }
177 $this->scan->status = $task;
178 $this->scan->save();
179 }
180
181 return false;
182 }
183
184 /**
185 * Reindex ignored issues to update their status in the scan model.
186 *
187 * @param Scan_Model $model The scan model.
188 */
189 private function reindex_ignored_issues( $model ) {
190 $issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE );
191 $ignore_lists = array();
192 foreach ( $issues as $issue ) {
193 $data = $issue->raw_data;
194 if ( isset( $data['file'] ) ) {
195 $ignore_lists[] = $data['file'];
196 } elseif ( isset( $data['slug'] ) ) {
197 $ignore_lists[] = $data['slug'];
198 }
199 }
200 $model->update_ignore_list( $ignore_lists );
201 }
202
203 /**
204 * Get a list of tasks will run in a scan.
205 *
206 * @return array
207 */
208 public function get_tasks(): array {
209 $this->settings = new Scan_Settings();
210 $tasks = array( Scan_Model::STEP_GATHER_INFO => 'gather_info' );
211 if ( $this->settings->integrity_check ) {
212 // Nested options.
213 if ( $this->settings->check_core ) {
214 $tasks[ Scan_Model::STEP_CHECK_CORE ] = 'core_integrity_check';
215 }
216 if ( $this->settings->check_plugins ) {
217 $tasks[ Scan_Model::STEP_CHECK_PLUGIN ] = 'plugin_integrity_check';
218 }
219 }
220 if ( $this->is_pro() ) {
221 if ( $this->settings->check_known_vuln ) {
222 if ( $this->has_method( Scan_Model::STEP_VULN_CHECK ) ) {
223 $tasks[ Scan_Model::STEP_VULN_CHECK ] = 'vuln_check';
224 }
225 }
226 if ( $this->settings->scan_malware ) {
227 if ( $this->has_method( Scan_Model::STEP_SUSPICIOUS_CHECK ) ) {
228 $tasks[ Scan_Model::STEP_SUSPICIOUS_CHECK ] = 'suspicious_check';
229 }
230 }
231 }
232
233 return $tasks;
234 }
235
236 /**
237 * Handles individual scan tasks based on the task identifier.
238 *
239 * @param string $task The task identifier.
240 *
241 * @return bool Returns true if the task was handled successfully.
242 */
243 private function task_handler( $task ) {
244 switch ( $task ) {
245 case 'gather_info':
246 if ( empty( $this->gather_fact ) && class_exists( Gather_Fact::class ) ) {
247 $this->set_gather_fact(
248 wd_di()->make( Gather_Fact::class, array( 'scan' => $this->scan ) )
249 );
250 }
251
252 return $this->gather_info( $this->gather_fact );
253 case 'vuln_check':
254 if ( empty( $this->known_vulnerability ) && class_exists( Known_Vulnerability::class ) ) {
255 $this->set_known_vulnerability(
256 wd_di()->make( Known_Vulnerability::class, array( 'scan' => $this->scan ) )
257 );
258 }
259
260 return $this->vuln_check( $this->known_vulnerability );
261
262 case 'suspicious_check':
263 if ( class_exists( Malware_Scan::class ) ) {
264 $this->set_malware_scan(
265 wd_di()->make( Malware_Scan::class, array( 'scan' => $this->scan ) )
266 );
267 }
268
269 return $this->suspicious_check( $this->malware_scan );
270
271 default:
272 return $this->$task();
273 }
274 }
275
276 /**
277 * A wrapper method for Known_Vulnerability class method vuln_check.
278 *
279 * @param Known_Vulnerability $known_vulnerability An instance of Known_Vulnerability.
280 *
281 * @return bool True always as in wrapped method Known_Vulnerability::vuln_check.
282 */
283 private function vuln_check( Known_Vulnerability $known_vulnerability ): bool {
284 if ( method_exists( $known_vulnerability, 'vuln_check' ) ) {
285 return $known_vulnerability->vuln_check();
286 }
287
288 return true; // Followed Known_Vulnerability::vuln_check return pattern i.e. always true for skipped vuln check.
289 }
290
291 /**
292 * Setter injection method for Known_Vulnerability instance.
293 *
294 * @param Known_Vulnerability $known_vulnerability The Known_Vulnerability instance to set.
295 */
296 public function set_known_vulnerability( Known_Vulnerability $known_vulnerability ) {
297 if ( class_exists( Known_Vulnerability::class ) ) {
298 $this->known_vulnerability = $known_vulnerability;
299 }
300 }
301
302 /**
303 * A wrapper method for Malware_Scan class method vuln_check.
304 *
305 * @param Malware_Scan $malware_scan An instance of Malware_Scan.
306 *
307 * @return bool True if method Malware_Scan::suspicious_check not exists else bool value returned by that method.
308 */
309 private function suspicious_check( Malware_Scan $malware_scan ): bool {
310 if ( method_exists( $malware_scan, 'suspicious_check' ) ) {
311 $quick_scan = wd_di()->get( Malware_Quick_Scan::class );
312 $deep_scan = wd_di()->get( Malware_Deep_Scan::class );
313
314 return $malware_scan->suspicious_check( $quick_scan, $deep_scan );
315 }
316
317 return true;
318 }
319
320 /**
321 * Setter injection method for Malware_Scan instance.
322 *
323 * @param Malware_Scan $malware_scan The Malware_Scan instance to set.
324 */
325 public function set_malware_scan( Malware_Scan $malware_scan ) {
326 if ( class_exists( Malware_Scan::class ) ) {
327 $this->malware_scan = $malware_scan;
328 }
329 }
330
331 /**
332 * Cancels an active scan and cleans up related data.
333 */
334 public function cancel_a_scan() {
335 $scan = Scan_Model::get_active();
336 if ( is_object( $scan ) ) {
337 $scan->delete();
338 }
339 $this->clean_up();
340 $this->remove_lock();
341
342 $scan_analytics = wd_di()->get( Scan_Analytics::class );
343
344 $scan_analytics->track_feature(
345 $scan_analytics::EVENT_SCAN_FAILED,
346 array(
347 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_CANCEL,
348 )
349 );
350 }
351
352 /**
353 * Track the event if we have a failed checksum.
354 */
355 public function maybe_track_failed_checksum() {
356 // If there is a Checksum issue, then check DB's value from time().
357 $checksum_issue = (int) get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 0 );
358 if ( $checksum_issue > 0 ) {
359 $scan_analytics = wd_di()->get( Scan_Analytics::class );
360 $reason = 'Failed to fetch checksums from wp.org';
361
362 $scan_analytics->track_feature(
363 $scan_analytics::EVENT_SCAN_FAILED,
364 array(
365 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
366 'Error_Reason' => $reason,
367 )
368 );
369
370 $this->log( $reason, Scan_Controller::SCAN_LOG );
371 }
372 }
373
374 /**
375 * Clean up data generate by current scan.
376 */
377 public function clean_up() {
378 $this->delete_interim_data();
379
380 $models = Scan_Model::get_last_all();
381 if ( ! empty( $models ) ) {
382 // Remove the latest. Don't remove code to find the first value.
383 $current = array_shift( $models );
384 foreach ( $models as $model ) {
385 $model->delete();
386 }
387 }
388 }
389
390 /**
391 * Get the total scanning active issues.
392 *
393 * @return int $count
394 */
395 public function indicator_issue_count(): int {
396 $count = 0;
397 $scan = Scan_Model::get_last();
398 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
399 // Only Scan issues.
400 $count = (int) $scan->count( null, Scan_Item::STATUS_ACTIVE );
401 }
402
403 return $count;
404 }
405
406 /**
407 * Checks if any scan type is active based on the scan settings and the user's membership status.
408 *
409 * @param array $scan_settings The scan settings.
410 * @param bool $is_pro Whether the user has a pro membership.
411 *
412 * @return bool Returns true if any scan type is active, false otherwise.
413 */
414 public function is_any_scan_active( $scan_settings, $is_pro ): bool {
415 if ( empty( $scan_settings['integrity_check'] ) ) {
416 // Check the parent type.
417 $file_change_check = false;
418 } elseif (
419 ! empty( $scan_settings['integrity_check'] )
420 && empty( $scan_settings['check_core'] )
421 && empty( $scan_settings['check_plugins'] )
422 ) {
423 // Check the parent and child types.
424 $file_change_check = false;
425 } else {
426 $file_change_check = true;
427 }
428 // Similar to is_any_active(...) method from the controller.
429 if ( $is_pro ) {
430 // Pro version. Check all parent types.
431 return $file_change_check || ! empty( $scan_settings['check_known_vuln'] ) || ! empty( $scan_settings['scan_malware'] );
432 } else {
433 // Free version. Check the 'File change detection' type because only it's available with nested types.
434 return $file_change_check;
435 }
436 }
437
438 /**
439 * Update the idle scan status due the time limit.
440 *
441 * @since 2.6.1
442 */
443 public function update_idle_scan_status() {
444 $idle_scan = wd_di()->get( Scan_Model::class )->get_idle();
445
446 if ( is_object( $idle_scan ) ) {
447 $ready_to_send = false;
448 if ( Scan_Model::STATUS_IDLE === $idle_scan->status ) {
449 $ready_to_send = true;
450 }
451 $this->delete_interim_data();
452
453 as_unschedule_all_actions( 'defender/async_scan' );
454
455 $idle_scan->status = Scan_Model::STATUS_IDLE;
456 $idle_scan->task_checkpoint = 'time_limit';
457 $idle_scan->save();
458
459 $this->remove_lock();
460 if ( $ready_to_send ) {
461 do_action( 'defender_notify', 'malware-notification', $idle_scan );
462 }
463 }
464 }
465
466 /**
467 * Update the idle scan status due the checksum issue.
468 *
469 * @param object $scan The current scan.
470 *
471 * @since 4.9.0
472 */
473 public function update_idle_scan_status_by_checksum_issue( $scan ) {
474 $ready_to_send = false;
475 if ( Scan_Model::STATUS_IDLE === $scan->status ) {
476 $ready_to_send = true;
477 }
478 $this->delete_interim_data();
479
480 as_unschedule_all_actions( 'defender/async_scan' );
481
482 $scan->status = Scan_Model::STATUS_IDLE;
483 $scan->task_checkpoint = 'checksum_issue';
484 $scan->save();
485
486 $this->remove_lock();
487 if ( $ready_to_send ) {
488 do_action( 'defender_notify', 'malware-notification', $scan );
489 }
490 }
491
492 /**
493 * Clear all temporary scan data.
494 *
495 * @since 2.6.1
496 */
497 private function delete_interim_data() {
498 delete_site_option( Gather_Fact::CACHE_CORE );
499 delete_site_option( Gather_Fact::CACHE_CONTENT );
500 delete_site_option( Malware_Scan::YARA_RULES );
501 delete_site_option( Core_Integrity::CACHE_CHECKSUMS );
502 delete_site_option( Plugin_Integrity::PLUGIN_SLUGS );
503 delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS );
504 $this->maybe_track_failed_checksum();
505 }
506
507 /**
508 * Display styles on the Plugins page.
509 */
510 public function show_plugin_admin_styles() {
511 $custom_css = '.vulnerability-indent{ padding-left: 26px; }
512 .plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%);
513 border-bottom: rgb(0 0 0 / 10%) solid 1px;}';
514 wp_add_inline_style( 'defender-menu', $custom_css );
515 }
516
517 /**
518 * Display update information for a plugin.
519 *
520 * @param string $file Plugin basename.
521 * @param array $plugin_data Plugin information.
522 *
523 * @return void
524 */
525 public function attach_plugin_vulnerability_warning( $file, $plugin_data ) {
526 /**
527 * WP Plugin list table instance.
528 *
529 * @var WP_Plugins_List_Table $wp_list_table
530 */
531 $wp_list_table = _get_list_table(
532 'WP_Plugins_List_Table',
533 array( 'screen' => get_current_screen() )
534 );
535 $bugs = $this->vulnerability_details[ $file ]['bugs'];
536 if ( empty( $bugs ) ) {
537 return;
538 }
539 $last_fixed_in = '0';
540 // Check if there have been updates since the last scan.
541 $exist_update = true;
542 if ( isset( $plugin_data['Version'] ) && ! empty( $plugin_data['Version'] ) ) {
543 // The current plugin version.
544 $current_version = $plugin_data['Version'];
545 foreach ( $bugs as $bug_details ) {
546 // If the fixed version is existed then get the latest one.
547 if ( isset( $bug_details['fixed_in'] ) && ! empty( $bug_details['fixed_in'] )
548 && version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' )
549 ) {
550 $last_fixed_in = $bug_details['fixed_in'];
551 }
552 }
553 if ( version_compare( $last_fixed_in, $current_version, '>' ) ) {
554 $exist_update = false;
555 }
556 }
557 // If there were updates, do not display notice.
558 if ( $exist_update ) {
559 return;
560 }
561 if ( empty( $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) {
562 $plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug'];
563 }
564
565 if ( is_network_admin() || ! is_multisite() ) {
566 if ( is_network_admin() ) {
567 $active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
568 } else {
569 $active_class = is_plugin_active( $file ) ? ' active' : '';
570 }
571
572 printf(
573 '<tr class="plugin-update-tr%s" id="vulnerability-%s" data-slug="%s" data-plugin="%s"><td colspan="%s" class="plugin-update colspanchange plugin-vulnerability"><div class="update-message notice inline %s notice-alt"><p>',
574 esc_attr( $active_class ),
575 esc_attr( $plugin_data['slug'] ),
576 esc_attr( $plugin_data['slug'] ),
577 esc_attr( $file ),
578 esc_attr( $wp_list_table->get_column_count() ),
579 'notice-error'
580 );
581
582 $notice = sprintf(
583 /* translators: %s - Plugin name. */
584 esc_html__(
585 '%s has detected a vulnerability in this plugin that may cause harm to your site.',
586 'defender-security'
587 ),
588 '<b>' . esc_html__( 'Defender Pro', 'defender-security' ) . '</b>'
589 );
590 if ( ( is_array( $bugs ) || $bugs instanceof Countable ? count( $bugs ) : 0 ) > 1 ) {
591 $notice .= '<hr/>';
592 $lines = array();
593 foreach ( $bugs as $bug ) {
594 $lines[] = '<span class="vulnerability-indent"></span>' . $bug['title'];
595 }
596 $notice .= implode( '<br/>', $lines );
597 $notice .= '<hr/><span class="vulnerability-indent"></span>';
598 if ( '0' !== $last_fixed_in ) {
599 $notice .= sprintf(
600 /* translators: %s - Version number. */
601 esc_html__(
602 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
603 'defender-security'
604 ),
605 $last_fixed_in
606 );
607 } else {
608 $notice .= esc_html__(
609 'Important! We recommend that you deactivate this plugin until the vulnerability has been fixed.',
610 'defender-security'
611 );
612 }
613 } else {
614 $notice .= '<br/><span class="vulnerability-indent"></span>' . $bugs[0]['title'] . '<br/><span class="vulnerability-indent"></span>';
615 $notice .= empty( $last_fixed_in )
616 ? esc_html__(
617 'We recommend that you deactivate this plugin until the vulnerability has been fixed.',
618 'defender-security'
619 )
620 : sprintf(
621 /* translators: 1: Version number. */
622 esc_html__(
623 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
624 'defender-security'
625 ),
626 $last_fixed_in
627 );
628 }
629
630 printf( wp_kses_post( $notice ) );
631 }
632 }
633
634 /**
635 * Display warnings.
636 *
637 * @since 2.6.2
638 */
639 public function display_vulnerability_warnings() {
640 if ( ! current_user_can( 'update_plugins' ) ) {
641 return;
642 }
643
644 $last = Scan_Model::get_last();
645 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
646 $vulnerability_issues = $last->get_issues( Scan_Item::TYPE_VULNERABILITY );
647 if ( empty( $vulnerability_issues ) ) {
648 return;
649 }
650
651 add_action( 'admin_print_styles-plugins.php', array( $this, 'show_plugin_admin_styles' ) );
652 // Vulnerability list.
653 foreach ( $vulnerability_issues as $vulnerability_obj ) {
654 $plugin_slug = $vulnerability_obj->raw_data['slug'];
655 // Get the details so that you can apply them later for each plugin.
656 $this->vulnerability_details[ $plugin_slug ] = $vulnerability_obj->raw_data;
657 add_action(
658 "after_plugin_row_$plugin_slug",
659 array(
660 $this,
661 'attach_plugin_vulnerability_warning',
662 ),
663 100,
664 2
665 );
666 }
667 }
668 }
669
670 /**
671 * Clear completed action scheduler logs.
672 *
673 * @since 2.6.5
674 */
675 public static function clear_logs() {
676 global $wpdb;
677
678 $table_actions = ! empty( $wpdb->actionscheduler_actions ) ?
679 $wpdb->actionscheduler_actions :
680 $wpdb->prefix . 'actionscheduler_actions';
681 $table_logs = ! empty( $wpdb->actionscheduler_logs ) ?
682 $wpdb->actionscheduler_logs :
683 $wpdb->prefix . 'actionscheduler_logs';
684
685 $table_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
686 $wpdb->prepare(
687 'SELECT count(*)
688 FROM information_schema.tables
689 WHERE table_schema = %s AND table_name IN (%s, %s);',
690 $wpdb->dbname,
691 $table_actions,
692 $table_logs
693 )
694 );
695
696 if ( 2 !== $table_count ) {
697 return array( 'error' => esc_html__( 'Action scheduler is not setup', 'defender-security' ) );
698 }
699
700 $hook = 'defender/async_scan';
701 $status = 'complete';
702 $limit = 100;
703 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
704 $wpdb->prepare(
705 "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
706 $hook,
707 $status,
708 $limit
709 )
710 );
711 while ( $action_ids ) {
712 if ( empty( $action_ids ) ) {
713 break;
714 }
715
716 $query = "DELETE as_actions, as_logs FROM {$table_actions} as_actions LEFT JOIN {$table_logs} as_logs ON as_actions.action_id = as_logs.action_id WHERE as_actions.action_id IN ( " . implode(
717 ', ',
718 array_fill(
719 0,
720 is_array( $action_ids ) || $action_ids instanceof Countable ? count( $action_ids ) : 0,
721 '%s'
722 )
723 ) . ' )';
724 $query = call_user_func_array(
725 array( $wpdb, 'prepare' ),
726 array_merge(
727 array( $query ),
728 $action_ids
729 )
730 );
731 // SQL is prepared here, so we will ignore WordPress.DB.PreparedSQL.NotPrepared.
732 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
733
734 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
735 $wpdb->prepare(
736 "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
737 $hook,
738 $status,
739 $limit
740 )
741 );
742 }
743
744 return array( 'success' => esc_html__( 'Malware scan logs are cleared', 'defender-security' ) );
745 }
746
747 /**
748 * Set the Gather_Fact object.
749 *
750 * @param Gather_Fact $gather_fact The Gather_Fact object to set.
751 */
752 public function set_gather_fact( Gather_Fact $gather_fact ) {
753 if ( class_exists( Gather_Fact::class ) ) {
754 $this->gather_fact = $gather_fact;
755 }
756 }
757
758 /**
759 * Gathers information using the Gather_Fact class.
760 *
761 * @param Gather_Fact $gather_fact The Gather_Fact instance.
762 *
763 * @return bool Returns true if information gathering is successful.
764 */
765 public function gather_info( Gather_Fact $gather_fact ): bool {
766 if ( method_exists( $gather_fact, 'gather_info' ) ) {
767 return $gather_fact->gather_info();
768 }
769
770 return true;
771 }
772
773 /**
774 * Gey intentions.
775 *
776 * @since 4.11.0
777 * @return array
778 */
779 public static function get_intentions(): array {
780 return array(
781 'resolve',
782 'ignore',
783 'delete',
784 'unignore',
785 'quarantine',
786 );
787 }
788 }
789