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 / model / class-scan.php

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

834 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles interaction with the database for scans.
4 *
5 * @package WP_Defender\Model
6 */
7
8 namespace WP_Defender\Model;
9
10 use WP_Error;
11 use DateTime;
12 use Countable;
13 use DateTimeZone;
14 use WP_Defender\DB;
15 use WP_Defender\Traits\IO;
16 use WP_Defender\Traits\Formats;
17 use WP_Defender\Component\Error_Code;
18 use WP_Defender\Behavior\Scan_Item\Core_Integrity;
19 use WP_Defender\Behavior\Scan_Item\Plugin_Integrity;
20
21 /**
22 * Model for scan table.
23 */
24 class Scan extends DB {
25
26 use IO;
27 use Formats;
28
29 public const STATUS_INIT = 'init', STATUS_ERROR = 'error', STATUS_FINISH = 'finish';
30 // Default state.
31 public const STEP_GATHER_INFO = 'gather_info';
32 public const STEP_CHECK_CORE = 'core_integrity_check', STEP_CHECK_PLUGIN = 'plugin_integrity_check';
33 public const STEP_VULN_CHECK = 'vuln_check', STEP_SUSPICIOUS_CHECK = 'suspicious_check';
34 public const IGNORE_INDEXER = 'defender_scan_ignore_index';
35
36 /**
37 * Table name.
38 *
39 * @var string
40 */
41 protected $table = 'defender_scan';
42
43 /**
44 * Any valid relative Date and Time formats.
45 *
46 * @link https://www.php.net/manual/en/datetime.formats.relative.php
47 * @since 2.6.1
48 * @var string
49 */
50 public const THRESHOLD_PERIOD = '3 hours ago';
51
52 /**
53 * Constant to notate the scan is idle or crossed the threshold limit.
54 *
55 * @since 2.6.1
56 * @var string
57 */
58 public const STATUS_IDLE = 'idle';
59
60 /**
61 * Primary key column.
62 *
63 * @var int
64 * @defender_property
65 */
66 public $id;
67 /**
68 * Table column for the status.
69 * Possible values are,
70 * - init
71 * - error
72 * - finish
73 * - gather_fact
74 * - core_integrity_check
75 * - plugin_integrity_check
76 * - vuln_check
77 * - suspicious_check
78 * - idle
79 *
80 * @var string
81 * @defender_property
82 */
83 public $status;
84 /**
85 * Table column for the start time.
86 *
87 * @var string
88 * @defender_property
89 */
90 public $date_start;
91
92 /**
93 * Table column for the current percentage.
94 *
95 * @var int
96 * @defender_property
97 */
98 public $percent = 0;
99
100 /**
101 * Table column for the total tasks.
102 * Store how many tasks we process.
103 *
104 * @var int
105 * @defender_property
106 */
107 public $total_tasks = 0;
108
109 /**
110 * Table column for the current task checkpoint.
111 *
112 * @var string
113 * @defender_property
114 */
115 public $task_checkpoint = '';
116
117 /**
118 * Table column for the end time.
119 *
120 * @var string
121 * @defender_property
122 */
123 public $date_end;
124
125 /**
126 * Table column for the scan trigger by report schedule.
127 *
128 * @var bool
129 * @defender_property
130 */
131 public $is_automation = false;
132
133 /**
134 * Prepare and fetch issues with various counts.
135 * This method retrieves active and ignored issues based on the specified type, page, and items per page.
136 * It prepares a detailed summary including total counts and filtered counts by type.
137 *
138 * @param int|null $per_page Number of items per page. Default null.
139 * @param int|null $paged Current page number. Default null.
140 * @param string|null $type Type of issues to filter. Default null.
141 *
142 * @return array An array containing the list of issues, ignored issues, and various count statistics.
143 * - 'ignored' (array): List of ignored issues.
144 * - 'issues' (array): List of active issues.
145 * - 'count_total' (int): Total number of active issues.
146 * - 'count_issues' (int): Total number of issues of all types.
147 * - 'count_issues_filtered' (int): Number of issues filtered by type.
148 * - 'count_ignored' (int): Total number of ignored issues.
149 * - 'count_core' (int): Number of core integrity issues.
150 * - 'count_plugin' (int): Number of plugin check issues.
151 * - 'count_malware' (int): Number of suspicious/malware issues.
152 * - 'count_vuln' (int): Number of vulnerability issues.
153 */
154 public function prepare_issues( $per_page = null, $paged = null, $type = null ): array {
155 $ignored_models = $this->get_issues( $type, Scan_Item::STATUS_IGNORE, $per_page, $paged );
156 $active_models = $this->get_issues( $type, Scan_Item::STATUS_ACTIVE, $per_page, $paged );
157
158 $issues = array();
159 $ignored = array();
160 $count_total = count( $active_models );
161 $count_issues_filtered = 0;
162
163 $scan_item_group_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
164
165 $count_issues = ! empty( $scan_item_group_total['all'] ) ?
166 $scan_item_group_total['all'] : 0;
167 $count_core = ! empty( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
168 $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
169 $count_plugin = ! empty( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
170 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
171 $count_malware = ! empty( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
172 $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
173 $count_vuln = ! empty( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
174 $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
175
176 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
177
178 $count_ignored = ! empty( $scan_item_ignore_total['all'] ) ?
179 $scan_item_ignore_total['all'] : 0;
180
181 foreach ( $ignored_models as $model ) {
182 $ignored[] = $model->to_array();
183 }
184 foreach ( $active_models as $active_model ) {
185 $issues[] = $active_model->to_array();
186
187 // We will now count all issues again by type filter for pagination usage.
188 if ( null !== $type && 'all' !== $type ) {
189 if ( $type === $active_model->type ) {
190 ++$count_issues_filtered;
191 }
192 } else {
193 ++$count_issues_filtered;
194 }
195 }
196
197 return array(
198 'ignored' => $ignored,
199 'issues' => $issues,
200 'count_total' => $count_total,
201 'count_issues' => $count_issues,
202 'count_issues_filtered' => $count_issues_filtered,
203 'count_ignored' => $count_ignored,
204 'count_core' => $count_core,
205 'count_plugin' => $count_plugin,
206 'count_malware' => $count_malware,
207 'count_vuln' => $count_vuln,
208 );
209 }
210
211 /**
212 * Retrieves scan issues based on provided filters.
213 * This method fetches scan items related to the current object's ID,
214 * filtered by type, status, and pagination parameters.
215 * The retrieved items are then attached with relevant behaviors based on their type.
216 *
217 * @param string|null $type Optional. The type of scan issue to filter by.
218 * Accepts 'vulnerability', 'integrity', 'plugin_check', or 'suspicious'. Default null.
219 * @param string|null $status Optional. The status of the scan issue to filter by.
220 * Accepts 'ignore' or 'active'. Default null.
221 * @param int|null $per_page Optional. The number of items to retrieve per page. Default null.
222 * @param int|null $paged Optional. The page number of items to retrieve. Default null.
223 *
224 * @return array An array of scan issue models with attached behaviors.
225 */
226 public function get_issues( $type = null, $status = null, $per_page = null, $paged = null ) {
227 $orm = self::get_orm();
228 $builder = $orm->get_repository( Scan_Item::class )
229 ->where( 'parent_id', $this->id );
230
231 if (
232 ! is_null( $type )
233 && in_array(
234 $type,
235 array(
236 Scan_Item::TYPE_VULNERABILITY,
237 Scan_Item::TYPE_INTEGRITY,
238 Scan_Item::TYPE_PLUGIN_CHECK,
239 Scan_Item::TYPE_SUSPICIOUS,
240 ),
241 true
242 )
243 ) {
244 $builder->where( 'type', $type );
245 }
246 if (
247 ! is_null( $status )
248 && in_array( $status, array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ), true )
249 ) {
250 $builder->where( 'status', $status );
251 }
252 if ( ! is_null( $per_page ) && ! is_null( $paged ) ) {
253 $limit = ( ( $paged - 1 ) * $per_page ) . ',' . $per_page;
254 $builder->limit( $limit );
255 }
256 $models = $builder->get();
257 foreach ( $models as $key => $model ) {
258 switch ( $model->type ) {
259 case Scan_Item::TYPE_INTEGRITY:
260 $model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
261 break;
262 case Scan_Item::TYPE_PLUGIN_CHECK:
263 $model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
264 break;
265 default:
266 break;
267 }
268 $models[ $key ] = $model;
269 }
270
271 return $models;
272 }
273
274 /**
275 * Counts the number of Scan_Item models that match the given type and status.
276 *
277 * @param string|null $type The type of Scan_Item to count. Must be one of the following:
278 * Scan_Item::TYPE_VULNERABILITY, Scan_Item::TYPE_INTEGRITY,
279 * Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::TYPE_SUSPICIOUS.
280 * @param string|null $status The status of the Scan_Item to count. Must be one of the following:
281 * Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE.
282 *
283 * @return mixed The number of matching Scan_Item models.
284 */
285 public function count( $type = null, $status = null ) {
286 $orm = self::get_orm();
287 $builder = $orm->get_repository( Scan_Item::class )->where( 'parent_id', $this->id );
288
289 if (
290 ! is_null( $type )
291 && in_array(
292 $type,
293 array(
294 Scan_Item::TYPE_VULNERABILITY,
295 Scan_Item::TYPE_INTEGRITY,
296 Scan_Item::TYPE_PLUGIN_CHECK,
297 Scan_Item::TYPE_SUSPICIOUS,
298 ),
299 true
300 )
301 ) {
302 $builder->where( 'type', $type );
303 }
304 if (
305 ! is_null( $status )
306 && in_array( $status, array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ), true )
307 ) {
308 $builder->where( 'status', $status );
309 }
310
311 return $builder->count();
312 }
313
314 /**
315 * Allow a specific issue by updating its status and removing it from the global ignore indexer.
316 *
317 * @param int $id The ID of the issue to Allow.
318 *
319 * @return bool|void Returns false if the issue does not exist, otherwise void.
320 */
321 public function unignore_issue( $id ) {
322 $issue = $this->get_issue( $id );
323 if ( ! is_object( $issue ) ) {
324 return false;
325 }
326 $issue->status = Scan_Item::STATUS_ACTIVE;
327 $issue->save();
328
329 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
330 $data = $issue->raw_data;
331 if ( isset( $data['file'] ) ) {
332 unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
333 } elseif ( isset( $data['slug'] ) ) {
334 unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
335 }
336 $this->update_ignore_list( $ignore_lists );
337 }
338
339 /**
340 * Check if a slug is ignored, we use a global indexer, so we can check while
341 * the active scan is running.
342 *
343 * @param string $slug The path to file.
344 *
345 * @return bool
346 */
347 public function is_issue_ignored( $slug ) {
348 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
349
350 return in_array( $slug, $ignore_lists, true );
351 }
352
353 /**
354 * Ignore a specific issue by updating its status and adding it to the global ignore indexer.
355 *
356 * @param int $id The ID of the issue to ignore.
357 *
358 * @return bool|void Returns false if the issue does not exist, otherwise void.
359 */
360 public function ignore_issue( $id ) {
361 $issue = $this->get_issue( $id );
362 if ( ! is_object( $issue ) ) {
363 return false;
364 }
365
366 $issue->status = Scan_Item::STATUS_IGNORE;
367 $issue->save();
368
369 // Add this into global ignore index.
370 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
371 $data = $issue->raw_data;
372 if ( isset( $data['file'] ) ) {
373 $ignore_lists[] = $data['file'];
374 } elseif ( isset( $data['slug'] ) ) {
375 $ignore_lists[] = $data['slug'];
376 }
377 $this->update_ignore_list( $ignore_lists );
378 }
379
380 /**
381 * Retrieves a Scan_Item object based on the given ID.
382 *
383 * @param int $id The ID of the Scan_Item.
384 *
385 * @return Scan_Item|null The Scan_Item object if found, null otherwise.
386 */
387 public function get_issue( $id ) {
388 $orm = self::get_orm();
389 $model = $orm->get_repository( Scan_Item::class )
390 ->where( 'id', $id )
391 ->first();
392
393 if ( is_object( $model ) ) {
394 switch ( $model->type ) {
395 case Scan_Item::TYPE_INTEGRITY:
396 $model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
397 break;
398 case Scan_Item::TYPE_PLUGIN_CHECK:
399 $model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
400 break;
401 default:
402 break;
403 }
404 }
405
406 return $model;
407 }
408
409 /**
410 * Remove an issue, this will happen when that issue is resolve, or the file link to this issue get deleted.
411 *
412 * @param int $id The ID of the issue to remove.
413 */
414 public function remove_issue( $id ) {
415 $orm = self::get_orm();
416 $orm->get_repository( Scan_Item::class )->delete( array( 'id' => $id ) );
417 }
418
419 /**
420 * Remove other Scan issue(-s) for the same file.
421 *
422 * @param string $path The path to file.
423 * @param string $type The type of scan issue.
424 *
425 * @return void
426 */
427 public function remove_related_issue_by( string $path, string $type ) {
428 // No needs to separate check Scan_Item::TYPE_VULNERABILITY because we do not delete per file for that type.
429 $orm = self::get_orm();
430 $builder = $orm->get_repository( Scan_Item::class )
431 ->where( 'parent_id', $this->id );
432 if ( '' !== $path ) {
433 $builder->where( 'type', 'NOT IN', array( $type, Scan_Item::TYPE_VULNERABILITY ) );
434 } else {
435 $builder->where( 'type', 'NOT IN', array( Scan_Item::TYPE_VULNERABILITY ) );
436 }
437 $models = $builder->get();
438
439 if ( ! empty( $models ) ) {
440 foreach ( $models as $model ) {
441 if ( $model->raw_data['file'] === $path ) {
442 $this->remove_issue( $model->id );
443 }
444 }
445 }
446 }
447
448 /**
449 * Converts the object to an array representation.
450 *
451 * @param int|null $per_page The number of items to retrieve per page. Default null.
452 * @param int|null $paged The page number of items to retrieve. Default null.
453 * @param string|null $type The type of scan issue to filter by. Default null.
454 *
455 * @return array The array representation of the object.
456 */
457 public function to_array( $per_page = null, $paged = null, $type = null ) {
458 if ( ! in_array( $this->status, array( self::STATUS_ERROR, self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
459
460 return array(
461 'status' => $this->status,
462 'status_text' => $this->get_status_text(),
463 'percent' => $this->percent,
464 'task_checkpoint' => $this->task_checkpoint,
465 // This only for hub, when a scan running.
466 'count' => array( 'total' => 0 ),
467 );
468 } elseif ( in_array( $this->status, array( self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
469 $total_filtered = (int) $this->count( $type );
470 $count_issues_filtered = (int) $this->count( $type, Scan_Item::STATUS_ACTIVE );
471 $total_count = (int) $this->count( null, Scan_Item::STATUS_ACTIVE );
472
473 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )
474 ->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
475
476 $count_ignored = ! empty( $scan_item_ignore_total['all'] ) ?
477 $scan_item_ignore_total['all'] : 0;
478
479 $total_issue_pages = 1;
480 $total_ignored_pages = 1;
481 if ( ! is_null( $per_page ) && ( $total_count > $per_page ) ) {
482 $data = $this->prepare_issues( $per_page, $paged, $type );
483 if ( ! is_null( $paged ) ) {
484 $total_issue_pages = ceil( $count_issues_filtered / $per_page );
485 $total_ignored_pages = ceil( $count_ignored / $per_page );
486 }
487 } else {
488 $data = $this->prepare_issues( null, null, $type );
489 }
490
491 $scan_item_group_total = wd_di()->get( Scan_Item::class )
492 ->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
493
494 $count_issues = ! empty( $scan_item_group_total['all'] ) ?
495 $scan_item_group_total['all'] : 0;
496 $count_core = ! empty( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
497 $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
498 $count_plugin = ! empty( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
499 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
500 $count_malware = ! empty( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
501 $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
502 $count_vuln = ! empty( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
503 $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
504
505 return array(
506 'status' => $this->status,
507 'issues_items' => $data['issues'],
508 'ignored_items' => $data['ignored'],
509 'last_scan' => $this->format_date_time( $this->date_start ),
510 'count' => array(
511 'total' => is_array( $data['issues'] ) || $data['issues'] instanceof Countable ? count( $data['issues'] ) : 0,
512 'total_filtered' => $total_filtered,
513 'issues_total' => $count_issues,
514 'issues_total_filtered' => $count_issues_filtered,
515 'ignored_total' => $count_ignored,
516 'core' => $count_core + $count_plugin,
517 'content' => $count_malware,
518 'vuln' => $count_vuln,
519 ),
520 'paging' => array(
521 'issue' => array(
522 'paged' => $paged,
523 'total_pages' => $total_issue_pages,
524 ),
525 'ignored' => array(
526 'paged' => $paged,
527 'total_pages' => $total_ignored_pages,
528 ),
529 'per_page' => $per_page,
530 ),
531 'task_checkpoint' => $this->task_checkpoint,
532 );
533 } else {
534 return array();
535 }
536 }
537
538 /**
539 * Creates a new record in the database.
540 *
541 * @param bool $from_report Is this a scan from report.
542 *
543 * @return bool|WP_Error|Scan
544 */
545 public static function create( $from_report = false ) {
546 $orm = self::get_orm();
547 $active = self::get_active();
548 if ( is_object( $active ) ) {
549 return new WP_Error( Error_Code::INVALID, esc_html__( 'A scan is already in progress.', 'defender-security' ) );
550 }
551 $model = new Scan();
552 $model->status = self::STATUS_INIT;
553 $model->date_start = gmdate( 'Y-m-d H:i:s' );
554 $model->date_end = gmdate( 'Y-m-d H:i:s' );
555 $model->is_automation = $from_report;
556
557 $orm->save( $model );
558
559 return $model;
560 }
561
562 /**
563 * Delete current scan.
564 *
565 * @param int|null $id Table primary key id.
566 */
567 public function delete( $id = null ) {
568 if ( ! $this->is_positive_int( $id ) ) {
569 $id = $this->id;
570 }
571
572 // Delete all the related result items.
573 $orm = self::get_orm();
574
575 $orm->get_repository( Scan_Item::class )->delete(
576 array( 'parent_id' => $id )
577 );
578
579 $orm->get_repository( self::class )->delete(
580 array( 'id' => $id )
581 );
582 }
583
584 /**
585 * Get the current active scan if any.
586 *
587 * @return self|null
588 */
589 public static function get_active() {
590 $orm = self::get_orm();
591
592 return $orm->get_repository( self::class )
593 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR, self::STATUS_IDLE ) )
594 ->first();
595 }
596
597 /**
598 * Check if the current state is Core integrity.
599 *
600 * @return self|null
601 */
602 public static function get_core_check() {
603 $orm = self::get_orm();
604
605 return $orm->get_repository( self::class )
606 ->where( 'status', self::STEP_CHECK_CORE )
607 ->first();
608 }
609
610 /**
611 * Get last result.
612 *
613 * @return self|null
614 */
615 public static function get_last() {
616 $orm = self::get_orm();
617
618 return $orm->get_repository( self::class )
619 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
620 ->order_by( 'id', 'desc' )
621 ->first();
622 }
623
624 /**
625 * Get last results.
626 *
627 * @return array
628 */
629 public static function get_last_all() {
630 $orm = self::get_orm();
631
632 return $orm->get_repository( self::class )
633 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
634 ->order_by( 'id', 'desc' )
635 ->get();
636 }
637
638 /**
639 * Adds an item to the scan.
640 *
641 * @param mixed $type The type of the item.
642 * @param mixed $data The data of the item.
643 * @param string $status The status of the item. Default is Scan_Item::STATUS_ACTIVE.
644 *
645 * @return bool Returns true if the item is successfully added, false otherwise.
646 */
647 public function add_item( $type, $data, $status = Scan_Item::STATUS_ACTIVE ) {
648 $model = new Scan_Item();
649 $model->type = $type;
650 $model->parent_id = $this->id;
651 $model->raw_data = $data;
652 $model->status = $status;
653 $ret = $model->save();
654
655 return $ret;
656 }
657
658 /**
659 * Return current status as readable string.
660 *
661 * @return string
662 */
663 public function get_status_text() {
664 switch ( $this->status ) {
665 case self::STATUS_INIT:
666 return esc_html__( 'Initializing...', 'defender-security' );
667 case self::STEP_GATHER_INFO:
668 return esc_html__( 'Gathering information...', 'defender-security' );
669 case self::STEP_CHECK_CORE:
670 return esc_html__( 'Analyzing WordPress Core...', 'defender-security' );
671 case self::STEP_CHECK_PLUGIN:
672 return esc_html__( 'Analyzing WordPress Plugins...', 'defender-security' );
673 case self::STEP_VULN_CHECK:
674 return esc_html__( 'Checking for any published vulnerabilities in your plugins and themes...', 'defender-security' );
675 case self::STEP_SUSPICIOUS_CHECK:
676 return esc_html__( 'Analyzing WordPress Content...', 'defender-security' );
677 default:
678 return esc_html__( 'The scan is running', 'defender-security' );
679 }
680 }
681
682 /**
683 * Calculates the percentage of a task based on its progress and position.
684 *
685 * @param int $task_percent The percentage of the task completed.
686 * @param int $pos The position of the task in the list of tasks. Default is 1.
687 *
688 * @return float The calculated percentage.
689 */
690 public function calculate_percent( $task_percent, $pos = 1 ) {
691 $task_max = ( 0 !== $this->total_tasks ) ? ( 100 / $this->total_tasks ) : 0;
692 $task_base = $task_max * ( $pos - 1 );
693 $micro = $task_percent * $task_max / 100;
694 $this->percent = round( $task_base + $micro, 2 );
695 if ( $this->percent > 100 ) {
696 $this->percent = 100;
697 }
698
699 return $this->percent;
700 }
701
702 /**
703 * Get list of whitelisted files.
704 *
705 * @return array
706 */
707 private function whitelisted_files() {
708 return array(
709 // Configuration files.
710 'user.ini',
711 'php.ini',
712 'robots.txt',
713 '.htaccess',
714 'nginx.conf',
715 // Hidden system files and directories.
716 '.well-known',
717 '.idea',
718 '.DS_Store',
719 '.svn',
720 '.git',
721 '.quarantine',
722 '.tmb',
723 '.vscode',
724 );
725 }
726
727 /**
728 * Check if a slug is whitelisted.
729 *
730 * @param string $slug The path to file.
731 *
732 * @return bool
733 */
734 public function is_issue_whitelisted( $slug ) {
735 $whitelisted_files = $this->whitelisted_files();
736 foreach ( $whitelisted_files as $file ) {
737 if ( false !== stristr( $slug, $file ) ) {
738 return true;
739 }
740 }
741
742 return false;
743 }
744
745 /**
746 * Update ignore list.
747 *
748 * @param array $ignore_lists Items to be added to the ignore list.
749 */
750 public function update_ignore_list( $ignore_lists ) {
751 $ignore_lists = array_unique( $ignore_lists );
752 $ignore_lists = array_filter( $ignore_lists );
753 update_site_option( self::IGNORE_INDEXER, $ignore_lists );
754 }
755
756 /**
757 * Get the threshold time limit as DateTime object.
758 *
759 * @return DateTime Threshold time limit as DateTime object.
760 */
761 public function threshold_date_time_object() {
762 $timezone = new DateTimeZone( 'UTC' );
763
764 /**
765 * Filter to override scan threshold period.
766 *
767 * @param string $threshold Any valid relative Date and Time formats.
768 *
769 * @link https://www.php.net/manual/en/datetime.formats.relative.php
770 * @since 2.6.1
771 */
772 $threshold = apply_filters( 'wd_scan_threshold', self::THRESHOLD_PERIOD );
773
774 return new DateTime( $threshold, $timezone );
775 }
776
777 /**
778 * Threshold time limit in mysql string format.
779 *
780 * @return string Threshold time limit as mysql string format.
781 */
782 public function threshold_date_time_mysql() {
783 $type = 'Y-m-d H:i:s';
784 $threshold_date_time_object = $this->threshold_date_time_object();
785 $mysql_format = $threshold_date_time_object->format( $type );
786
787 return $mysql_format;
788 }
789
790 /**
791 * Get the idle scan if any.
792 *
793 * @return self|null
794 */
795 public function get_idle() {
796 $orm = self::get_orm();
797
798 $mysql_date = $this->threshold_date_time_mysql();
799
800 return $orm->get_repository( self::class )
801 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR ) )
802 ->where( 'date_start', '<', $mysql_date )
803 ->first();
804 }
805
806 /**
807 * Delete all idle scan and scan items
808 *
809 * @since 2.6.1
810 */
811 public function delete_idle() {
812 $idle_scans = self::get_orm()
813 ->get_repository( self::class )
814 ->where( 'status', self::STATUS_IDLE )
815 ->get();
816
817 foreach ( $idle_scans as $idle_scan ) {
818 $this->delete( $idle_scan->id );
819 }
820 }
821
822 /**
823 * Verify positive integer or not.
824 *
825 * @param mixed $id Argument to check for a positive number.
826 *
827 * @return bool Return true on positive integer else false.
828 * @since 2.6.1
829 */
830 private function is_positive_int( $id ) {
831 return is_int( $id ) && $id > 0;
832 }
833 }
834