PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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-quarantine.php

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

966 lines 27.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Component class for handling quarantine functionalities within WP Defender.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Traits\IO;
11 use Calotes\Base\Component;
12 use WP_Defender\Traits\Plugin;
13 use WP_Defender\Traits\Formats;
14 use WP_Defender\Model\Scan_Item;
15 use WP_Defender\Behavior\WPMUDEV;
16 use WP_Defender\Helper\File as File_Helper;
17 use WP_Defender\Model\Setting\Main_Setting;
18 use WP_Defender\Component\Scheduler\Scheduler;
19 use WP_Defender\Model\Setting\Scan as Scan_Setting;
20 use WP_Defender\Model\Quarantine as Quarantine_Model;
21 use WP_Filesystem_Base;
22
23 /**
24 * Service layer for quarantine files functionality.
25 *
26 * @since 4.0.0
27 */
28 class Quarantine extends Component {
29
30 use Plugin;
31 use Formats;
32 use IO;
33
34 /**
35 * Quarantine file expiry limit.
36 *
37 * @var ?int
38 */
39 private $file_expiry_timestamp;
40
41 /**
42 * Quarantine model instance.
43 *
44 * @var Quarantine_Model
45 */
46 private $quarantine_model;
47
48 /**
49 * Scheduler instance.
50 *
51 * @var Scheduler
52 */
53 private $scheduler;
54
55 /**
56 * Scan setting instance.
57 *
58 * @var Scan_Setting
59 */
60 private $scan_setting;
61
62 /**
63 * Main setting instance.
64 *
65 * @var Main_Setting
66 */
67 private $main_setting;
68
69 /**
70 * WPMUDEV instance.
71 *
72 * @var WPMUDEV
73 */
74 private $wpmudev;
75
76 /**
77 * Hook tag to delete expired files.
78 */
79 private const CRON_HOOK = 'wpdef_quarantine_delete_expired';
80
81 /**
82 * Quarantine directory name.
83 */
84 private const QUARANTINE_DIRECTORY = '.defender-security-quarantine';
85
86 /**
87 * Quarantine directory permission.
88 */
89 public const QUARANTINE_DIRECTORY_PERMISSION = 0755;
90
91 /**
92 * Constructor for the Quarantine component.
93 */
94 public function __construct() {
95 // Dependencies.
96 $this->quarantine_model = wd_di()->get( Quarantine_Model::class );
97 $this->scheduler = wd_di()->get( Scheduler::class );
98 $this->scan_setting = wd_di()->get( Scan_Setting::class );
99 $this->main_setting = wd_di()->get( Main_Setting::class );
100 $this->wpmudev = wd_di()->get( WPMUDEV::class );
101
102 $this->file_expiry_timestamp = $this->set_expiry_timestamp();
103
104 $this->init();
105 }
106
107 /**
108 * Get file permission in octal number.
109 *
110 * @param string $file File path.
111 *
112 * @return int File permission octal notation.
113 */
114 private function get_octet_fileperms( $file ): int {
115 clearstatcache();
116
117 return (int) decoct( fileperms( $file ) & 0777 );
118 }
119
120 /**
121 * Prepare value for all columns of quarantine table.
122 *
123 * @param Scan_Item $scan_item Scan item model object.
124 *
125 * @return Quarantine_Model Quarantine model object.
126 */
127 private function prepare_file_metadata( Scan_Item $scan_item ): Quarantine_Model {
128 $file = $scan_item->raw_data['file'];
129
130 $file_hash = (string) sha1_file( $file ) . uniqid();
131
132 $path_info = pathinfo( $file );
133
134 $mime = (string) mime_content_type( $file );
135
136 $perms = $this->get_octet_fileperms( $file );
137
138 $file_owner = (string) fileowner( $file );
139
140 $file_group = (string) filegroup( $file );
141
142 $plugin_headers = $this->get_plugin_headers( $file );
143 $plugin_headers = reset( $plugin_headers );
144
145 $file_version = isset( $plugin_headers['Version'] ) ? $plugin_headers['Version'] : '';
146
147 $mtime = wp_date( 'Y-m-d H:i:s', (int) filemtime( $file ) );
148
149 $source_slug = $this->get_plugin_directory_name( $file );
150
151 $created_time = wp_date( 'Y-m-d H:i:s' );
152
153 $created_by = get_current_user_id();
154
155 $file_category = $this->quarantine_model::WP_PLUGIN;
156
157 $this->quarantine_model->defender_scan_item_id = $scan_item->id;
158 $this->quarantine_model->file_hash = $file_hash;
159 $this->quarantine_model->file_full_path = $file;
160 $this->quarantine_model->file_original_name = $path_info['filename'];
161 $this->quarantine_model->file_extension = isset( $path_info['extension'] ) ? $path_info['extension'] : '';
162 $this->quarantine_model->file_mime_type = $mime;
163 $this->quarantine_model->file_rw_permission = $perms;
164 $this->quarantine_model->file_owner = $file_owner;
165 $this->quarantine_model->file_group = $file_group;
166 $this->quarantine_model->file_version = $file_version;
167 $this->quarantine_model->file_category = $file_category;
168 $this->quarantine_model->file_modified_time = $mtime;
169 $this->quarantine_model->source_slug = $source_slug;
170 $this->quarantine_model->created_time = $created_time;
171 $this->quarantine_model->created_by = $created_by;
172
173 return $this->quarantine_model;
174 }
175
176 /**
177 * Logics to process before file manipulation.
178 *
179 * @param string $file_path File path of the plugin.
180 *
181 * @return array Array of plugin data. Keys plugin_basename, is_active & is_network_active.
182 */
183 private function before_file_transaction( string $file_path ): array {
184 // Plugin active state.
185 $plugin_basename = plugin_basename( $file_path );
186 $is_active = is_plugin_active( $plugin_basename );
187 $is_network_active = is_plugin_active_for_network( $plugin_basename );
188
189 // Deactivate plugin which are already activated. No effect on already deactivated plugin.
190 if ( $is_active ) {
191 deactivate_plugins( $plugin_basename, true, $is_network_active );
192 }
193
194 $result = array(
195 'plugin_basename' => $plugin_basename,
196 'is_active' => $is_active,
197 'is_network_active' => $is_network_active,
198 );
199
200 $this->log_wrapper( $result );
201
202 return $result;
203 }
204
205 /**
206 * Logics to process before file manipulation.
207 *
208 * @param array $data Data needs to activate the plugin.
209 */
210 private function after_file_transaction( array $data ): void {
211 $this->log_wrapper( $data );
212
213 // Activate plugin if already activates.
214 if ( $data['is_active'] ) {
215 activate_plugins( $data['plugin_basename'], '', $data['is_network_active'], true );
216 }
217 }
218
219 /**
220 * Wrapper method which quarantine the file.
221 * Track plugin activation/deactivation state.
222 * Decide to deactivate plugin based on the state.
223 * Do core quarantine process.
224 * Reinstate previous state of plugin activation/deactivation.
225 *
226 * @param Scan_Item $scan_item Scan item model object.
227 * @param string $parent_action Parent action.
228 *
229 * @return array Index message: describes what happened.
230 * Index success: true if file quarantined and quarantine record
231 * created else false.
232 */
233 public function quarantine_file( Scan_Item $scan_item, string $parent_action ): array {
234 $before_file_transaction = $this->before_file_transaction( $scan_item->raw_data['file'] );
235
236 // Do quarantine file processing.
237 $action = $this->do_quarantine( $scan_item, $parent_action );
238
239 $this->after_file_transaction( $before_file_transaction );
240
241 $this->wpmudev->schedule_hub_sync();
242
243 return $action;
244 }
245
246 /**
247 * Transfer file from source to destination.
248 * Depends on the parent action if action is repair then copy file to destination.
249 * Else move file to destination.
250 *
251 * @param string $from Source file path.
252 * @param string $to Destination file path.
253 * @param string $parent_action Parent action. As of now only two actions repair or delete.
254 *
255 * @return bool True on operation succeeded or False on operation failed.
256 */
257 private function file_transfer( string $from, string $to, string $parent_action ): bool {
258 global $wp_filesystem;
259 if ( 'repair' === $parent_action ) {
260 return copy( $from, $to );
261 }
262 return $wp_filesystem->move( $from, $to, true );
263 }
264
265 /**
266 * Core method which Quarantine the file.
267 * Do only file processing and DB handling.
268 *
269 * @param Scan_Item $scan_item Scan item model object.
270 * @param string $parent_action Parent action.
271 *
272 * @return array Index message: describes what happened.
273 * Index success: true if file quarantined and quarantine record
274 * created else false.
275 */
276 private function do_quarantine( Scan_Item $scan_item, string $parent_action ): array {
277 $quarantine_model = $this->prepare_file_metadata( $scan_item );
278
279 $this->log_wrapper( $quarantine_model, 'Do quarantine: Prepare file metadata' );
280 $this->log_wrapper( $parent_action, 'Do quarantine: Parent action name' );
281
282 $file_name_with_extension = $quarantine_model->file_original_name . '.' . $quarantine_model->file_extension;
283
284 $is_renamed = $this->file_transfer(
285 $quarantine_model->file_full_path,
286 $this->get_quarantined_file_path( $quarantine_model->file_hash ),
287 $parent_action
288 );
289
290 if ( ! $is_renamed ) {
291 $result = array(
292 'message' => sprintf(
293 /* translators: 1: Filename with extension */
294 esc_html__( 'Failed to quarantine the file %1$s.', 'defender-security' ),
295 '<strong>' . $file_name_with_extension . '</strong>'
296 ),
297 'success' => false,
298 );
299
300 $this->log_wrapper( $result );
301
302 return $result;
303 }
304
305 $saved = $quarantine_model->save();
306
307 if ( ! is_int( $saved ) ) {
308 $result = array(
309 'message' => sprintf(
310 /* translators: 1: Filename with extension */
311 esc_html__( 'Failed to add quarantine record in DB for the file %1$s.', 'defender-security' ),
312 '<strong>' . $file_name_with_extension . '</strong>'
313 ),
314 'success' => false,
315 );
316
317 $this->log_wrapper( $result );
318
319 return $result;
320 }
321
322 $message = sprintf(
323 /* translators: 1: Filename with extension */
324 esc_html__( 'Quarantined the file %1$s and deleted the source file successfully.', 'defender-security' ),
325 '<strong>' . $file_name_with_extension . '</strong>'
326 );
327
328 if ( 'repair' === $parent_action ) {
329 $plugin_write_handler = $this->plugin_write_handler( $quarantine_model );
330
331 if ( false === $plugin_write_handler['success'] ) {
332 $this->log_wrapper( $plugin_write_handler, 'Plugin write failed' );
333
334 $this->restore_file( $scan_item );
335
336 return $plugin_write_handler;
337 }
338
339 $message = sprintf(
340 /* translators: 1: Filename with extension */
341 esc_html__(
342 'Quarantined the file %1$s and repaired the source file successfully.',
343 'defender-security'
344 ),
345 '<strong>' . $file_name_with_extension . '</strong>'
346 );
347 }
348
349 $result = array(
350 'message' => $message,
351 'success' => true,
352 );
353
354 $scan_item->delete_by_id( $scan_item->id );
355
356 $this->log_wrapper( $result );
357
358 return $result;
359 }
360
361 /**
362 * Restore the quarantined file either using Scan_Item object or Quarantine record primary key.
363 *
364 * @param Scan_Item|int $entity To determine which file to restore.
365 *
366 * @return array Index message: describes what happened.
367 * Index success: true if file moved and quarantine record
368 * removed else false.
369 */
370 public function restore_file( $entity ): array {
371 global $wp_filesystem;
372 // Initialize the WP filesystem, no more using 'file-put-contents' function.
373 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
374 require_once ABSPATH . '/wp-admin/includes/file.php';
375 WP_Filesystem();
376 }
377 if ( $entity instanceof Scan_Item ) {
378 $file_metadata = $this->get_record_by_scan_item( $entity );
379 } elseif ( is_int( $entity ) ) {
380 $file_metadata = $this->quarantine_model->find_by_id( $entity );
381 }
382
383 $this->log_wrapper( $file_metadata );
384
385 if (
386 ! isset(
387 $file_metadata,
388 $file_metadata->file_hash,
389 $file_metadata->file_full_path,
390 $file_metadata->id,
391 $file_metadata->file_original_name,
392 $file_metadata->file_extension
393 )
394 ) {
395 $result = array(
396 'message' => esc_html__( 'Record not exists in DB.', 'defender-security' ),
397 'success' => false,
398 );
399
400 $this->log_wrapper( $result );
401
402 return $result;
403 }
404
405 $file_name_with_extension = $file_metadata->file_original_name . '.' . $file_metadata->file_extension;
406
407 if ( ! $this->is_quarantine_file_exists( $file_metadata->file_hash ) ) {
408 $result = array(
409 'message' => esc_html__( 'Quarantined file doesn\'t exist in the quarantine directory.', 'defender-security' ),
410 'success' => false,
411 );
412
413 $this->log_wrapper(
414 $result,
415 'External error: File System Error or Quarantined file deleted by someone using OS CMD'
416 );
417
418 return $result;
419 }
420
421 $plugin_headers = $this->get_plugin_headers( $file_metadata->file_full_path );
422
423 if ( array() === $plugin_headers ) {
424 $result = array(
425 'message' => sprintf(
426 /* translators: 1: Filename with extension */
427 esc_html__( 'Plugin main file missing for %1$s file.', 'defender-security' ),
428 '<strong>' . $file_name_with_extension . '</strong>'
429 ),
430 'success' => false,
431 );
432
433 $this->log_wrapper( $result );
434 }
435
436 $quarantined_file_path = $this->get_quarantined_file_path( $file_metadata->file_hash );
437
438 $before_file_transaction = $this->before_file_transaction( $file_metadata->file_full_path );
439
440 $is_renamed = $wp_filesystem->move( $quarantined_file_path, $file_metadata->file_full_path, true );
441
442 $this->after_file_transaction( $before_file_transaction );
443
444 if ( ! $is_renamed ) {
445 $result = array(
446 'message' => esc_html__( 'Failed to restore quarantined file.', 'defender-security' ),
447 'success' => false,
448 );
449
450 $this->log_wrapper( $result );
451
452 return $result;
453 }
454
455 $is_delete_success = $this->quarantine_model->delete( (int) $file_metadata->id );
456
457 if ( ! $is_delete_success ) {
458 $result = array(
459 'message' => esc_html__( 'Failed to remove quarantine record from DB.', 'defender-security' ),
460 'success' => false,
461 );
462
463 $this->log_wrapper( $result );
464
465 return $result;
466 }
467
468 $result = array(
469 'message' => sprintf(
470 /* translators: 1: Filename with extension */
471 esc_html__( 'Restored %1$s', 'defender-security' ),
472 '<strong>' . $file_name_with_extension . '</strong>'
473 ),
474 'success' => true,
475 );
476
477 $this->log_wrapper( $result );
478
479 $this->wpmudev->schedule_hub_sync();
480
481 return $result;
482 }
483
484 /**
485 * Return Quarantine model.
486 *
487 * @param Scan_Item $scan_item Scan_Item object which individual record of scan data.
488 *
489 * @return mixed Return Quarantine model if exists.
490 */
491 private function get_record_by_scan_item( Scan_Item $scan_item ) {
492 return $this->quarantine_model->select_restore_detail( $scan_item );
493 }
494
495 /**
496 * Check if a file is quarantined.
497 *
498 * @param Scan_Item $scan_item The scan item to check.
499 *
500 * @return bool True if the file is quarantined, false otherwise.
501 */
502 private function is_quarantined( Scan_Item $scan_item ): bool {
503 $quarantined_record = $this->quarantine_model->select_by_file_full_path( $scan_item->raw_data['file'] );
504
505 $is_file_exists = false;
506
507 if ( isset( $quarantined_record[0] ) ) {
508 $is_file_exists = $this->is_quarantine_file_exists( $quarantined_record[0]->file_hash );
509 }
510
511 return $is_file_exists;
512 }
513
514 /**
515 * Get the path to the quarantine directory.
516 *
517 * @return string The path to the quarantine directory.
518 */
519 private function get_quarantine_directory(): string {
520 global $wp_filesystem;
521 // Initialize the WP filesystem, no more using 'file-put-contents' function.
522 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
523 require_once ABSPATH . '/wp-admin/includes/file.php';
524 WP_Filesystem();
525 }
526 $quarantine_dir = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . self::QUARANTINE_DIRECTORY;
527
528 if ( ! is_dir( $quarantine_dir ) ) {
529 $wp_filesystem->mkdir( $quarantine_dir, self::QUARANTINE_DIRECTORY_PERMISSION );
530 }
531 $file_helper = wd_di()->get( File_Helper::class );
532 $file_helper->maybe_dir_access_deny( $quarantine_dir );
533
534 return $quarantine_dir;
535 }
536
537 /**
538 * Get the full path to a quarantined file.
539 *
540 * @param string $file_hash The hash of the file.
541 *
542 * @return string The full path to the quarantined file.
543 */
544 private function get_quarantined_file_path( string $file_hash ): string {
545 return $this->get_quarantine_directory() . DIRECTORY_SEPARATOR . $file_hash . '.restore';
546 }
547
548 /**
549 * Writes plugin file by fetching from wp.org repo.
550 *
551 * @param Quarantine_Model $quarantine_model SQL ORM object.
552 *
553 * @return array Index message: describes what happened.
554 * Index success: true if writes successfully
555 * else false on failed to write or WP plugin repository errors.
556 */
557 private function plugin_write_handler( Quarantine_Model $quarantine_model ): array {
558 global $wp_filesystem;
559 // Initialize the WP filesystem, no more using 'file-put-contents' function.
560 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
561 require_once ABSPATH . '/wp-admin/includes/file.php';
562 WP_Filesystem();
563 }
564 $plugin_directory_name = $this->get_plugin_directory_name( $quarantine_model->file_full_path );
565
566 if ( ! $this->is_likely_wporg_slug( $plugin_directory_name ) ) {
567 return array(
568 'message' => esc_html__( 'Make sure plugin exists in WordPress plugin repository.', 'defender-security' ),
569 'success' => false,
570 );
571 }
572
573 $plugin_relative_path = $this->get_plugin_relative_path( $quarantine_model->file_full_path );
574 $is_plugin_in_wp_org = $this->check_plugin_on_wp_org( $plugin_directory_name );
575
576 $file_name_with_extension = $quarantine_model->file_original_name . '.' . $quarantine_model->file_extension;
577 $generic_error = array(
578 'message' => sprintf(
579 /* translators: 1: Filename with extension */
580 esc_html__( 'Failed to write the file %1$s.', 'defender-security' ),
581 '<strong>' . $file_name_with_extension . '</strong>'
582 ),
583 'success' => false,
584 );
585
586 if (
587 isset( $is_plugin_in_wp_org['success'] ) &&
588 true === $is_plugin_in_wp_org['success']
589 ) {
590 $file_url = $this->get_file_url(
591 $plugin_directory_name,
592 $quarantine_model->file_version,
593 $plugin_relative_path
594 );
595
596 $file_content = $this->get_url_content( $file_url );
597
598 $is_written = $wp_filesystem->put_contents( $quarantine_model->file_full_path, $file_content );
599
600 if ( $is_written ) {
601 return array(
602 'message' => sprintf(
603 /* translators: 1: Filename with extension */
604 esc_html__( 'Writes the file %1$s successfully.', 'defender-security' ),
605 '<strong>' . $file_name_with_extension . '</strong>'
606 ),
607 'success' => true,
608 );
609 }
610
611 return $generic_error;
612 } elseif (
613 isset( $is_plugin_in_wp_org['success'], $is_plugin_in_wp_org['message'] ) &&
614 false === $is_plugin_in_wp_org['success']
615 ) {
616 $error = $is_plugin_in_wp_org['message'];
617
618 return array(
619 'message' => sprintf(
620 /* translators: 1: WordPress API error message */
621 esc_html__( 'WordPress remote error: %1$s', 'defender-security' ),
622 '<strong>' . $error . '</strong>'
623 ),
624 'success' => false,
625 );
626 }
627
628 return $generic_error;
629 }
630
631 /**
632 * Check if a quarantined file exists.
633 *
634 * @param string $file_hash The hash of the file.
635 *
636 * @return bool True if the file exists, false otherwise.
637 */
638 private function is_quarantine_file_exists( string $file_hash ): bool {
639 return file_exists( $this->get_quarantined_file_path( $file_hash ) );
640 }
641
642 /**
643 * Get a collection of quarantined files.
644 *
645 * @return array An array of quarantined files.
646 */
647 public function quarantine_collection(): array {
648 $collections = $this->quarantine_model->quarantine_collection();
649
650 foreach ( $collections as $index => $collection ) {
651 $name = '';
652 // Is the plugin file?
653 if ( 0 === strpos( realpath( $collection['file_full_path'] ), realpath( WP_PLUGIN_DIR ) ) ) {
654 $plugin_headers = $this->get_plugin_headers( $collection['file_full_path'] );
655 if ( is_array( $plugin_headers ) ) {
656 $plugin_headers = reset( $plugin_headers );
657
658 $name = isset( $plugin_headers['Name'] ) ? $plugin_headers['Name'] : '';
659 }
660 }
661
662 $collections[ $index ]['name'] = $name;
663
664 $file_modified_timestamp = strtotime( $collection['file_modified_time'] );
665 $created_timestamp = strtotime( $collection['created_time'] );
666
667 $collections[ $index ]['file_modified_time'] = false !== $file_modified_timestamp
668 ? wp_date( 'M j, Y', $file_modified_timestamp )
669 : $collection['file_modified_time'];
670
671 $collections[ $index ]['created_time'] = false !== $created_timestamp
672 ? wp_date( 'M j, Y', $created_timestamp )
673 : $collection['created_time'];
674 }
675
676 return $collections;
677 }
678
679 /**
680 * Hard delete the quarantined file and remove the DB record.
681 *
682 * @param int $id Primary key of the record need to be deleted with associated file.
683 *
684 * @return bool True on success else false.
685 */
686 public function delete_quarantined_file( int $id ): bool {
687 $file_metadata = $this->quarantine_model->find_by_id( $id );
688
689 $this->log_wrapper( $file_metadata, 'On deletion: File metadata' );
690
691 $is_file_exists = $this->is_quarantine_file_exists( $file_metadata->file_hash );
692
693 $this->log_wrapper( 'File exists? ' . $is_file_exists );
694
695 if (
696 ! is_null( $file_metadata ) &&
697 $is_file_exists
698 ) {
699 wp_delete_file(
700 $this->get_quarantined_file_path(
701 $file_metadata->file_hash
702 )
703 );
704 $is_model_deleted = $this->quarantine_model->delete( $id );
705
706 $this->log_wrapper( 'Model deleted? ' . $is_model_deleted );
707
708 $scan = \WP_Defender\Model\Scan::get_last();
709 $scan->remove_related_issue_by( $file_metadata->file_full_path, '' );
710
711 $this->wpmudev->schedule_hub_sync();
712
713 return $is_model_deleted;
714 }
715
716 $this->log_wrapper( 'Check SQL row & quarantined file exists for PK: ' . $id );
717
718 return false;
719 }
720
721 /**
722 * Delete files which are older the expiry time limit.
723 */
724 private function delete_old_file(): void {
725 if ( is_int( $this->file_expiry_timestamp ) ) {
726 $expiry_limit = wp_date( 'Y-m-d H:i:s', $this->file_expiry_timestamp );
727
728 $old_records = $this->quarantine_model->get_old_records( $expiry_limit );
729
730 foreach ( $old_records as $file_id ) {
731 $this->delete_quarantined_file( (int) $file_id['id'] );
732 }
733 }
734 }
735
736 /**
737 * Cron schedule delete old files.
738 */
739 public function cron_process(): void {
740 $this->delete_old_file();
741 }
742
743 /**
744 * Invoke all init methods.
745 */
746 public function init(): void {
747 $interval = $this->scheduler->get_cron_schedule_interval(
748 $this->scan_setting->quarantine_expire_schedule
749 );
750
751 if ( false !== $interval ) {
752 /**
753 * Delete old quarantined files.
754 *
755 * @var Network_Cron_Manager $network_cron_manager
756 */
757 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
758 $network_cron_manager->register_callback(
759 self::CRON_HOOK,
760 array( $this, 'cron_process' ),
761 $interval
762 );
763 }
764
765 $this->get_quarantine_directory();
766 }
767
768 /**
769 * List of cron schedules utilized by quarantine expiry settings.
770 *
771 * @return array List of WP cron schedules relevant to quarantine period.
772 */
773 public function cron_schedules(): array {
774 return $this->scheduler->filter_cron_schedules(
775 array(
776 'thirty_days',
777 'sixty_days',
778 'ninety_days',
779 'six_months',
780 'one_year',
781 )
782 );
783 }
784
785 /**
786 * Calculate threshold timestamp.
787 * This timestamp is used to delete sql record created before threshold
788 * timestamp and it's associated file.
789 *
790 * @return ?int Threshold timestamp.
791 */
792 private function set_expiry_timestamp(): ?int {
793 $schedule_name = $this->scan_setting->quarantine_expire_schedule;
794
795 $get_schedule = $this->scheduler->filter_cron_schedules(
796 array( $schedule_name )
797 );
798
799 if ( isset( $get_schedule[ $schedule_name ] ) ) {
800 $now = strtotime( 'now' );
801 $now = ! is_int( $now ) ? (int) $now : $now;
802 $quarantined_time_threshold = $now - $get_schedule[ $schedule_name ]['interval'];
803
804 return $quarantined_time_threshold;
805 }
806
807 return null;
808 }
809
810 /**
811 * Reschedule the cron.
812 * This method helps to change schedule when expiry period changed and delete
813 * quarantine sql records and associated file if the quarantined time is expired.
814 *
815 * @param string $prev Previously selected cron interval.
816 * @param string $current Currently selected cron interval.
817 */
818 public function reschedule_file_expiry_cron( string $prev, string $current ): void {
819 // If prev cron interval and current cron interval same then early return.
820 if ( 0 === strcmp( $prev, $current ) ) {
821 return;
822 }
823
824 // Else if prev cron interval not equal to current cron interval.
825 // Replace the previous scheduler with new.
826 $this->scheduler->override_schedule( self::CRON_HOOK, $current );
827
828 // Invoke cron process immediately to delete expired file related to current interval.
829 $this->cron_process();
830 }
831
832 /**
833 * Get quarantine directory URL.
834 *
835 * @return string Quarantine directory URL.
836 */
837 public function quarantine_directory_url(): string {
838 return content_url( self::QUARANTINE_DIRECTORY );
839 }
840
841 /**
842 * Actions to accomplish before plugin uninstallation.
843 * If Remove settings chosen then directly remove data i.e. without archiving the quarantined file and table data.
844 */
845 public function on_uninstall(): void {
846 if ( 'remove' === $this->main_setting->uninstall_quarantine ) {
847 $this->data_remove();
848 }
849 }
850
851 /**
852 * Remove quarantine file system & table data.
853 */
854 private function data_remove(): void {
855 $this->delete_dir( $this->get_quarantine_directory() );
856 $this->quarantine_model->drop_table();
857 }
858
859 /**
860 * Log wrapper method.
861 *
862 * @param mixed $message Details need to be write accepts string or array or object.
863 * @param string $custom_trace_title Description about custom trace event.
864 * @param bool $in_depth_debug If true in depth trace else if false then simple trace.
865 * @param int $in_depth_limit How much deep to trace. Works only for true assigned for $in_depth_debug.
866 * @param string $category Log filename.
867 */
868 private function log_wrapper(
869 $message,
870 string $custom_trace_title = '',
871 bool $in_depth_debug = false,
872 int $in_depth_limit = 1,
873 string $category = 'quarantine'
874 ): void {
875 $this->log( 'Backtrace Summary:', $category );
876 /**
877 * Ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
878 * Why?
879 * This is an internal function and it's using to debug error in production.
880 */
881 $this->log( wp_debug_backtrace_summary( null, 1, false ), $category ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
882
883 $this->log( 'Custom Trace:', $category );
884
885 if ( '' !== $custom_trace_title ) {
886 $this->log( $custom_trace_title, $category );
887 }
888
889 $this->log( $message, $category );
890
891 if ( true === $in_depth_debug ) {
892 $this->log( 'Detailed Debug:', $category );
893 /**
894 * Ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
895 * Why?
896 * This is an internal function and it's using to debug error in production.
897 */
898 $this->log( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $in_depth_limit ), $category ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
899 }
900 }
901
902 /**
903 * Data required from quarantine on scan item.
904 *
905 * @param Scan_Item $scan_item Scan item model object.
906 *
907 * @return array Array of data required from quarantine on scan item.
908 */
909 public function scan_item_data( Scan_Item $scan_item ): array {
910 return array(
911 'is_quarantined' => $this->is_quarantined( $scan_item ),
912 'is_quarantinable' => $this->is_quarantinable( $scan_item->raw_data['file'] ),
913 );
914 }
915
916 /**
917 * Array of quarantined files, and it's source file details for HUB widget listing.
918 *
919 * @return array Quarantined files details.
920 */
921 public function hub_list(): array {
922 $model_list = $this->quarantine_model->hub_list();
923
924 $hub_api_list = array();
925
926 foreach ( $model_list as $quarantine_item ) {
927 $id = $quarantine_item['id'];
928 $file_name = $quarantine_item['file_original_name'] . '.' . $quarantine_item['file_extension'];
929 $quarantined_time = strtotime( $quarantine_item['quarantined_time'] );
930 $quarantined_path = $this->get_quarantined_file_path( $quarantine_item['quarantined_path'] );
931 $source_path = $quarantine_item['source_path'];
932
933 array_push(
934 $hub_api_list,
935 compact(
936 'id',
937 'file_name',
938 'quarantined_time',
939 'quarantined_path',
940 'source_path'
941 )
942 );
943 }
944
945 return $hub_api_list;
946 }
947
948 /**
949 * Check the quarantine directory is forbidden.
950 * When user visits the URL of the quarantine directory it should not be available.
951 * This function returns false if we get the 200 status code and true for any other status codes.
952 *
953 * @return bool
954 */
955 public function is_quarantine_directory_url_forbidden(): bool {
956 $response = wp_remote_head(
957 $this->quarantine_directory_url(),
958 array( 'timeout' => 5 )
959 );
960
961 $is_200 = 200 === (int) wp_remote_retrieve_response_code( $response );
962
963 return ! $is_200;
964 }
965 }
966