| 1 |
<?php |
| 2 |
|
| 3 |
namespace WP_Defender\Component; |
| 4 |
|
| 5 |
use WP_Defender\Behavior\Scan\Core_Integrity; |
| 6 |
use WP_Defender\Behavior\Scan\Gather_Fact; |
| 7 |
use WP_Defender\Behavior\Scan\Known_Vulnerability; |
| 8 |
use WP_Defender\Behavior\Scan\Malware_Scan; |
| 9 |
use WP_Defender\Behavior\Scan\Plugin_Integrity; |
| 10 |
use WP_Defender\Behavior\WPMUDEV; |
| 11 |
use WP_Defender\Component; |
| 12 |
use WP_Defender\Model\Scan_Item; |
| 13 |
use WP_Defender\Model\Scan as Model_Scan; |
| 14 |
|
| 15 |
class Scan extends Component { |
| 16 |
|
| 17 |
/** |
| 18 |
* Cache the current scan. |
| 19 |
* |
| 20 |
* @var \WP_Defender\Model\Scan |
| 21 |
*/ |
| 22 |
public $scan; |
| 23 |
|
| 24 |
/** |
| 25 |
* @var \WP_Defender\Model\Setting\Scan |
| 26 |
*/ |
| 27 |
public $settings; |
| 28 |
|
| 29 |
/** |
| 30 |
* @var array |
| 31 |
*/ |
| 32 |
protected $vulnerability_details = array(); |
| 33 |
|
| 34 |
public function __construct() { |
| 35 |
$this->attach_behavior( WPMUDEV::class, WPMUDEV::class ); |
| 36 |
$this->attach_behavior( Gather_Fact::class, Gather_Fact::class ); |
| 37 |
$this->attach_behavior( Core_Integrity::class, Core_Integrity::class ); |
| 38 |
$this->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class ); |
| 39 |
if ( class_exists( Known_Vulnerability::class ) ) { |
| 40 |
$this->attach_behavior( Known_Vulnerability::class, new Known_Vulnerability() ); |
| 41 |
} |
| 42 |
if ( class_exists( Malware_Scan::class ) ) { |
| 43 |
$this->attach_behavior( Malware_Scan::class, new Malware_Scan() ); |
| 44 |
} |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* @param object $model |
| 49 |
*/ |
| 50 |
public function advanced_scan_actions( $model ) { |
| 51 |
$this->reindex_ignored_issues( $model ); |
| 52 |
$this->clean_up(); |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Process current scan. |
| 57 |
* |
| 58 |
* @return bool |
| 59 |
* @throws \ReflectionException |
| 60 |
*/ |
| 61 |
public function process() { |
| 62 |
$scan = Model_Scan::get_active(); |
| 63 |
if ( ! is_object( $scan ) ) { |
| 64 |
// This case can be a scan get cancel. |
| 65 |
return - 1; |
| 66 |
} |
| 67 |
$this->scan = $scan; |
| 68 |
$this->settings = new \WP_Defender\Model\Setting\Scan(); |
| 69 |
$tasks = $this->get_tasks(); |
| 70 |
$runner = new \ArrayIterator( $tasks ); |
| 71 |
$task = $this->scan->status; |
| 72 |
if ( Model_Scan::STATUS_INIT === $scan->status ) { |
| 73 |
// Get the first. |
| 74 |
$this->log( 'Prepare facts for a scan', 'scan.log' ); |
| 75 |
$task = 'gather_fact'; |
| 76 |
$this->scan->percent = 0; |
| 77 |
$this->scan->total_tasks = $runner->count(); |
| 78 |
$this->scan->save(); |
| 79 |
} |
| 80 |
if ( |
| 81 |
in_array( |
| 82 |
$this->scan->status, |
| 83 |
array( |
| 84 |
Model_Scan::STATUS_ERROR, |
| 85 |
Model_Scan::STATUS_IDLE, |
| 86 |
), |
| 87 |
true |
| 88 |
) |
| 89 |
) { |
| 90 |
// Stop and return true to abort the process. |
| 91 |
return true; |
| 92 |
} |
| 93 |
// Find the current task. |
| 94 |
$offset = array_search( $task, array_values( $tasks ) ); |
| 95 |
if ( false === $offset ) { |
| 96 |
$this->log( sprintf( 'offset is not found, search %s', $task ), 'scan.log' ); |
| 97 |
|
| 98 |
return false; |
| 99 |
} |
| 100 |
// Reset the tasks to current. |
| 101 |
$runner->seek( $offset ); |
| 102 |
$this->log( sprintf( 'Current task %s', $runner->current() ), 'scan.log' ); |
| 103 |
if ( $this->has_method( $task ) ) { |
| 104 |
$this->log( sprintf( 'processing %s', $runner->key() ), 'scan.log' ); |
| 105 |
$result = $this->$task(); |
| 106 |
if ( true === $result ) { |
| 107 |
$this->log( sprintf( 'task %s processed', $runner->key() ), 'scan.log' ); |
| 108 |
// Task is done, move to next. |
| 109 |
$runner->next(); |
| 110 |
if ( $runner->valid() ) { |
| 111 |
$this->log( sprintf( 'queue %s for next', $runner->key() ), 'scan.log' ); |
| 112 |
$this->scan->status = $runner->key(); |
| 113 |
$this->scan->task_checkpoint = ''; |
| 114 |
$this->scan->date_end = gmdate( 'Y-m-d H:i:s' ); |
| 115 |
$this->scan->save(); |
| 116 |
// Queue for next run. |
| 117 |
return false; |
| 118 |
} |
| 119 |
$this->log( 'All done!', 'scan.log' ); |
| 120 |
// No more task in the queue, we are done. |
| 121 |
$this->scan->status = Model_Scan::STATUS_FINISH; |
| 122 |
$this->scan->save(); |
| 123 |
$this->advanced_scan_actions( $this->scan ); |
| 124 |
do_action( 'defender_notify', 'malware-notification', $this->scan ); |
| 125 |
|
| 126 |
return true; |
| 127 |
} |
| 128 |
$this->scan->status = $task; |
| 129 |
$this->scan->save(); |
| 130 |
} |
| 131 |
|
| 132 |
return false; |
| 133 |
} |
| 134 |
|
| 135 |
/** |
| 136 |
* @param \WP_Defender\Model\Scan $model |
| 137 |
*/ |
| 138 |
private function reindex_ignored_issues( $model ) { |
| 139 |
$issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE ); |
| 140 |
$ignore_lists = array(); |
| 141 |
foreach ( $issues as $issue ) { |
| 142 |
$data = $issue->raw_data; |
| 143 |
if ( isset( $data['file'] ) ) { |
| 144 |
$ignore_lists[] = $data['file']; |
| 145 |
} elseif ( isset( $data['slug'] ) ) { |
| 146 |
$ignore_lists[] = $data['slug']; |
| 147 |
} |
| 148 |
} |
| 149 |
$model->update_ignore_list( $ignore_lists ); |
| 150 |
} |
| 151 |
|
| 152 |
/** |
| 153 |
* Get a list of tasks will run in a scan. |
| 154 |
* |
| 155 |
* @return array |
| 156 |
*/ |
| 157 |
public function get_tasks() { |
| 158 |
$tasks = array( |
| 159 |
'gather_fact' => 'gather_fact', |
| 160 |
); |
| 161 |
if ( $this->settings->integrity_check ) { |
| 162 |
/** |
| 163 |
* @since 2.4.7 |
| 164 |
*/ |
| 165 |
if ( $this->settings->check_core ) { |
| 166 |
$tasks['core_integrity_check'] = 'core_integrity_check'; |
| 167 |
} |
| 168 |
if ( $this->settings->check_plugins ) { |
| 169 |
$tasks['plugin_integrity_check'] = 'plugin_integrity_check'; |
| 170 |
} |
| 171 |
} |
| 172 |
if ( $this->is_pro() ) { |
| 173 |
if ( $this->settings->check_known_vuln ) { |
| 174 |
if ( $this->has_method( 'vuln_check' ) ) { |
| 175 |
$tasks['vuln_check'] = 'vuln_check'; |
| 176 |
} |
| 177 |
} |
| 178 |
if ( $this->settings->scan_malware ) { |
| 179 |
if ( $this->has_method( 'suspicious_check' ) ) { |
| 180 |
$tasks['suspicious_check'] = 'suspicious_check'; |
| 181 |
} |
| 182 |
} |
| 183 |
} |
| 184 |
|
| 185 |
return $tasks; |
| 186 |
} |
| 187 |
|
| 188 |
public function cancel_a_scan() { |
| 189 |
$scan = Model_Scan::get_active(); |
| 190 |
if ( is_object( $scan ) ) { |
| 191 |
$scan->delete(); |
| 192 |
} |
| 193 |
$this->clean_up(); |
| 194 |
$this->remove_lock(); |
| 195 |
} |
| 196 |
|
| 197 |
/** |
| 198 |
* Clean up data generate by current scan. |
| 199 |
*/ |
| 200 |
public function clean_up() { |
| 201 |
$this->delete_interim_data(); |
| 202 |
|
| 203 |
$models = Model_Scan::get_last_all(); |
| 204 |
if ( ! empty( $models ) ) { |
| 205 |
// Remove the latest. Don't remove code to find the first value. |
| 206 |
$current = array_shift( $models ); |
| 207 |
foreach ( $models as $model ) { |
| 208 |
$model->delete(); |
| 209 |
} |
| 210 |
} |
| 211 |
} |
| 212 |
|
| 213 |
/** |
| 214 |
* Create a file lock, so we can check if a process already running. |
| 215 |
*/ |
| 216 |
public function create_lock() { |
| 217 |
file_put_contents( $this->get_lock_path(), time(), LOCK_EX ); |
| 218 |
} |
| 219 |
|
| 220 |
/** |
| 221 |
* Delete file lock. |
| 222 |
*/ |
| 223 |
public function remove_lock() { |
| 224 |
@unlink( $this->get_lock_path() ); |
| 225 |
} |
| 226 |
|
| 227 |
/** |
| 228 |
* Check if a lock is valid. |
| 229 |
* |
| 230 |
* @return bool |
| 231 |
*/ |
| 232 |
public function has_lock() { |
| 233 |
if ( ! file_exists( $this->get_lock_path() ) ) { |
| 234 |
return false; |
| 235 |
} |
| 236 |
$time = file_get_contents( $this->get_lock_path() ); |
| 237 |
if ( strtotime( '+90 seconds', $time ) < time() ) { |
| 238 |
// Usually a timeout window is 30 seconds, so we should allow lock at 1.30min for safe. |
| 239 |
return false; |
| 240 |
} |
| 241 |
|
| 242 |
return true; |
| 243 |
} |
| 244 |
|
| 245 |
/** |
| 246 |
* Get the total scanning active issues. |
| 247 |
* |
| 248 |
* @return integer $count |
| 249 |
*/ |
| 250 |
public function indicator_issue_count() { |
| 251 |
$count = 0; |
| 252 |
$scan = Model_Scan::get_last(); |
| 253 |
if ( is_object( $scan ) && ! is_wp_error( $scan ) ) { |
| 254 |
// Only Scan issues. |
| 255 |
$count = $scan->count( null, Scan_Item::STATUS_ACTIVE ); |
| 256 |
} |
| 257 |
|
| 258 |
return $count; |
| 259 |
} |
| 260 |
|
| 261 |
/** |
| 262 |
* @param array $scan_settings |
| 263 |
* @param bool $is_pro |
| 264 |
* |
| 265 |
* @return bool |
| 266 |
*/ |
| 267 |
public function is_any_scan_active( $scan_settings, $is_pro ) { |
| 268 |
if ( empty( $scan_settings['integrity_check'] ) ) { |
| 269 |
$integrity_check = false; |
| 270 |
} elseif ( |
| 271 |
! empty( $scan_settings['integrity_check'] ) |
| 272 |
&& empty( $scan_settings['check_core'] ) |
| 273 |
&& empty( $scan_settings['check_plugins'] ) |
| 274 |
) { |
| 275 |
$integrity_check = false; |
| 276 |
} else { |
| 277 |
$integrity_check = true; |
| 278 |
} |
| 279 |
|
| 280 |
if ( ! $integrity_check && ! $is_pro ) { |
| 281 |
|
| 282 |
return false; |
| 283 |
} elseif ( |
| 284 |
! $integrity_check |
| 285 |
&& empty( $scan_settings['check_known_vuln'] ) |
| 286 |
&& empty( $scan_settings['scan_malware'] ) |
| 287 |
&& ! $is_pro |
| 288 |
) { |
| 289 |
|
| 290 |
return false; |
| 291 |
} |
| 292 |
|
| 293 |
return true; |
| 294 |
} |
| 295 |
|
| 296 |
/** |
| 297 |
* Update the idle scan status. |
| 298 |
* |
| 299 |
* @since 2.6.1 |
| 300 |
*/ |
| 301 |
public function update_idle_scan_status() { |
| 302 |
$idle_scan = wd_di()->get( Model_Scan::class )->get_idle(); |
| 303 |
|
| 304 |
if ( is_object( $idle_scan ) ) { |
| 305 |
$ready_to_send = false; |
| 306 |
if ( Model_Scan::STATUS_IDLE === $idle_scan->status ) { |
| 307 |
$ready_to_send = true; |
| 308 |
} |
| 309 |
$this->delete_interim_data(); |
| 310 |
|
| 311 |
as_unschedule_all_actions( 'defender/async_scan' ); |
| 312 |
|
| 313 |
$idle_scan->status = Model_Scan::STATUS_IDLE; |
| 314 |
$idle_scan->save(); |
| 315 |
|
| 316 |
$this->remove_lock(); |
| 317 |
if ( $ready_to_send ) { |
| 318 |
do_action( 'defender_notify', 'malware-notification', $idle_scan ); |
| 319 |
} |
| 320 |
} |
| 321 |
} |
| 322 |
|
| 323 |
/** |
| 324 |
* Clear all temporary scan data. |
| 325 |
* |
| 326 |
* @since 2.6.1 |
| 327 |
*/ |
| 328 |
private function delete_interim_data() { |
| 329 |
delete_site_option( Gather_Fact::CACHE_CORE ); |
| 330 |
delete_site_option( Gather_Fact::CACHE_CONTENT ); |
| 331 |
delete_site_option( Malware_Scan::YARA_RULES ); |
| 332 |
delete_site_option( Core_Integrity::CACHE_CHECKSUMS ); |
| 333 |
delete_site_option( Plugin_Integrity::PLUGIN_SLUGS ); |
| 334 |
delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS ); |
| 335 |
} |
| 336 |
|
| 337 |
/** |
| 338 |
* Display styles on the Plugins page. |
| 339 |
*/ |
| 340 |
public function show_plugin_admin_styles() { |
| 341 |
$custom_css = '.vulnerability-indent{ padding-left: 26px; } |
| 342 |
.plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%); |
| 343 |
border-bottom: rgb(0 0 0 / 10%) solid 1px;}'; |
| 344 |
wp_add_inline_style( 'defender-menu', $custom_css ); |
| 345 |
} |
| 346 |
|
| 347 |
/** |
| 348 |
* Display update information for a plugin. |
| 349 |
* @param string $file Plugin basename. |
| 350 |
* @param array $plugin_data Plugin information. |
| 351 |
* |
| 352 |
* @return void |
| 353 |
*/ |
| 354 |
public function attach_plugin_vulnerability_warning( $file, $plugin_data ) { |
| 355 |
/** @var WP_Plugins_List_Table $wp_list_table */ |
| 356 |
$wp_list_table = _get_list_table( |
| 357 |
'WP_Plugins_List_Table', |
| 358 |
array( |
| 359 |
'screen' => get_current_screen(), |
| 360 |
) |
| 361 |
); |
| 362 |
$bugs = $this->vulnerability_details[ $file ]['bugs']; |
| 363 |
if ( empty( $bugs ) ) { |
| 364 |
|
| 365 |
return; |
| 366 |
} |
| 367 |
$last_fixed_in = '0'; |
| 368 |
// Check if there have been updates since the last scan. |
| 369 |
$exist_update = true; |
| 370 |
if ( isset( $plugin_data['Version'] ) && ! empty( $plugin_data['Version'] ) ) { |
| 371 |
// The current plugin version. |
| 372 |
$current_version = $plugin_data['Version']; |
| 373 |
foreach ( $bugs as $bug_details ) { |
| 374 |
// If the fixed version is existed then get the latest one. |
| 375 |
if( isset( $bug_details['fixed_in'] ) |
| 376 |
&& version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' ) |
| 377 |
) { |
| 378 |
$last_fixed_in = $bug_details['fixed_in']; |
| 379 |
} |
| 380 |
} |
| 381 |
if( version_compare( $last_fixed_in, $current_version, '>' ) ) { |
| 382 |
$exist_update = false; |
| 383 |
} |
| 384 |
} |
| 385 |
// If there were updates, do not display notice. |
| 386 |
if ( $exist_update ) { |
| 387 |
|
| 388 |
return; |
| 389 |
} |
| 390 |
// Sometimes $plugin_data['slug'] is empty. |
| 391 |
if ( empty( $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) { |
| 392 |
$plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug']; |
| 393 |
} |
| 394 |
|
| 395 |
if ( is_network_admin() || ! is_multisite() ) { |
| 396 |
if ( is_network_admin() ) { |
| 397 |
$active_class = is_plugin_active_for_network( $file ) ? ' active' : ''; |
| 398 |
} else { |
| 399 |
$active_class = is_plugin_active( $file ) ? ' active' : ''; |
| 400 |
} |
| 401 |
|
| 402 |
printf( |
| 403 |
'<tr class="plugin-update-tr%s" id="vulnerability-%s" data-slug="%s" data-plugin="%s">' . |
| 404 |
'<td colspan="%s" class="plugin-update colspanchange plugin-vulnerability">' . |
| 405 |
'<div class="update-message notice inline %s notice-alt"><p>', |
| 406 |
$active_class, |
| 407 |
esc_attr( $plugin_data['slug'] ), |
| 408 |
esc_attr( $plugin_data['slug'] ), |
| 409 |
esc_attr( $file ), |
| 410 |
esc_attr( $wp_list_table->get_column_count() ), |
| 411 |
'notice-error' |
| 412 |
); |
| 413 |
|
| 414 |
$notice = sprintf( |
| 415 |
/* translators: %s - Plugin name. */ |
| 416 |
__( '%s has detected a vulnerability in this plugin that may cause harm to your site.', 'wpdef' ), |
| 417 |
'<b>' . __( 'Defender Pro', 'wpdef' ) . '</b>' |
| 418 |
); |
| 419 |
if ( (is_array($bugs) || $bugs instanceof \Countable ? count( $bugs ) : 0) > 1 ) { |
| 420 |
$notice .= '<hr/>'; |
| 421 |
$lines = array(); |
| 422 |
foreach ( $bugs as $bug ) { |
| 423 |
$lines[] = '<span class="vulnerability-indent"></span>' . $bug['title']; |
| 424 |
} |
| 425 |
$notice .= implode( '<br/>', $lines ); |
| 426 |
$notice .= '<hr/><span class="vulnerability-indent"></span>'; |
| 427 |
if ( '0' !== $last_fixed_in ) { |
| 428 |
$notice .= sprintf( |
| 429 |
/* translators: %s - Version number. */ |
| 430 |
__( 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.', 'wpdef' ), |
| 431 |
$last_fixed_in |
| 432 |
); |
| 433 |
} else { |
| 434 |
$notice .= __( 'Important! We recommend that you deactivate this plugin until the vulnerability has been fixed.', 'wpdef' ); |
| 435 |
} |
| 436 |
} else { |
| 437 |
$notice .= '<br/><span class="vulnerability-indent"></span>' . $bugs[0]['title'] . '<br/><span class="vulnerability-indent"></span>'; |
| 438 |
$notice .= empty( $last_fixed_in ) |
| 439 |
? __( 'We recommend that you deactivate this plugin until the vulnerability has been fixed.', 'wpdef' ) |
| 440 |
: sprintf( |
| 441 |
/* translators: 1: Version number. */ |
| 442 |
__( 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.', 'wpdef' ), |
| 443 |
$last_fixed_in |
| 444 |
); |
| 445 |
} |
| 446 |
|
| 447 |
printf( $notice ); |
| 448 |
} |
| 449 |
} |
| 450 |
|
| 451 |
/** |
| 452 |
* Display warnings. |
| 453 |
* @since 2.6.2 |
| 454 |
*/ |
| 455 |
public function display_vulnerability_warnings() { |
| 456 |
if ( ! current_user_can( 'update_plugins' ) ) { |
| 457 |
|
| 458 |
return; |
| 459 |
} |
| 460 |
|
| 461 |
$last = \WP_Defender\Model\Scan::get_last(); |
| 462 |
if ( is_object( $last ) && ! is_wp_error( $last ) ) { |
| 463 |
$vulnerability_issues = $last->get_issues( Scan_Item::TYPE_VULNERABILITY ); |
| 464 |
if ( empty( $vulnerability_issues ) ) { |
| 465 |
return; |
| 466 |
} |
| 467 |
|
| 468 |
add_action( 'admin_print_styles-plugins.php', array( $this, 'show_plugin_admin_styles' ) ); |
| 469 |
foreach ( $vulnerability_issues as $vulnerability_obj ) { |
| 470 |
$plugin_slug = $vulnerability_obj->raw_data['slug']; |
| 471 |
// Get the details so that you can apply them later for each plugin. |
| 472 |
$this->vulnerability_details[ $plugin_slug ] = $vulnerability_obj->raw_data; |
| 473 |
add_action( |
| 474 |
"after_plugin_row_$plugin_slug", |
| 475 |
array( |
| 476 |
$this, |
| 477 |
'attach_plugin_vulnerability_warning', |
| 478 |
), |
| 479 |
100, |
| 480 |
2 |
| 481 |
); |
| 482 |
} |
| 483 |
} |
| 484 |
} |
| 485 |
|
| 486 |
/** |
| 487 |
* Clear completed action scheduler logs. |
| 488 |
* |
| 489 |
* @since 2.6.5 |
| 490 |
*/ |
| 491 |
public static function clear_logs() { |
| 492 |
global $wpdb; |
| 493 |
|
| 494 |
$table_actions = ! empty( $wpdb->actionscheduler_actions ) ? |
| 495 |
$wpdb->actionscheduler_actions : |
| 496 |
$wpdb->prefix . 'actionscheduler_actions'; |
| 497 |
$table_logs = ! empty( $wpdb->actionscheduler_logs ) ? |
| 498 |
$wpdb->actionscheduler_logs : |
| 499 |
$wpdb->prefix . 'actionscheduler_logs'; |
| 500 |
|
| 501 |
$table_count = (int) $wpdb->get_var( |
| 502 |
$wpdb->prepare( |
| 503 |
"SELECT count(*) |
| 504 |
FROM information_schema.tables |
| 505 |
WHERE table_schema = %s AND table_name IN (%s, %s);", |
| 506 |
$wpdb->dbname, |
| 507 |
$table_actions, |
| 508 |
$table_logs |
| 509 |
) |
| 510 |
); |
| 511 |
|
| 512 |
if ( 2 !== $table_count ) { |
| 513 |
return array( |
| 514 |
'error' => __( 'Action scheduler is not setup', 'wpdef' ), |
| 515 |
); |
| 516 |
} |
| 517 |
|
| 518 |
$hook = 'defender/async_scan'; |
| 519 |
$status = 'complete'; |
| 520 |
$limit = 100; |
| 521 |
while ( $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", $hook, $status, $limit ) ) ) { |
| 522 |
if ( empty( $action_ids ) ) { |
| 523 |
break; |
| 524 |
} |
| 525 |
|
| 526 |
$where_in = implode( ', ', array_fill( 0, is_array($action_ids) || $action_ids instanceof \Countable ? count( $action_ids ) : 0, '%s' ) ); |
| 527 |
$r = $wpdb->query( |
| 528 |
$wpdb->prepare( |
| 529 |
"DELETE as_actions, as_logs |
| 530 |
FROM {$table_actions} as_actions |
| 531 |
LEFT JOIN {$table_logs} as_logs |
| 532 |
ON as_actions.action_id = as_logs.action_id |
| 533 |
WHERE as_actions.action_id IN ( {$where_in} )", |
| 534 |
$action_ids |
| 535 |
) |
| 536 |
); |
| 537 |
} |
| 538 |
|
| 539 |
return array( |
| 540 |
'success' => __( 'Malware scan logs are cleared', 'wpdef' ), |
| 541 |
); |
| 542 |
} |
| 543 |
} |
| 544 |
|