| 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 |
|