HEX
Server: Apache
System: Linux vps.teamads.com 4.18.0-553.126.1.el8_10.x86_64 #1 SMP Thu May 28 06:44:09 EDT 2026 x86_64
User: teamadsc (1024)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/teamadsc/public_html/wp-content/plugins/wp-defender/src/component/class-quarantine.php
<?php
/**
 * Component class for handling quarantine functionalities within WP Defender.
 *
 * @package WP_Defender\Component
 */

namespace WP_Defender\Component;

use WP_Defender\Traits\IO;
use Calotes\Base\Component;
use WP_Defender\Traits\Plugin;
use WP_Defender\Traits\Formats;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Helper\File as File_Helper;
use WP_Defender\Model\Setting\Main_Setting;
use WP_Defender\Component\Scheduler\Scheduler;
use WP_Defender\Model\Setting\Scan as Scan_Setting;
use WP_Defender\Model\Quarantine as Quarantine_Model;
use WP_Filesystem_Base;

/**
 * Service layer for quarantine files functionality.
 *
 * @since 4.0.0
 */
class Quarantine extends Component {

	use Plugin;
	use Formats;
	use IO;

	/**
	 * Quarantine file expiry limit.
	 *
	 * @var ?int
	 */
	private $file_expiry_timestamp;

	/**
	 * Quarantine model instance.
	 *
	 * @var Quarantine_Model
	 */
	private $quarantine_model;

	/**
	 * Scheduler instance.
	 *
	 * @var Scheduler
	 */
	private $scheduler;

	/**
	 * Scan setting instance.
	 *
	 * @var Scan_Setting
	 */
	private $scan_setting;

	/**
	 * Main setting instance.
	 *
	 * @var Main_Setting
	 */
	private $main_setting;

	/**
	 * WPMUDEV instance.
	 *
	 * @var WPMUDEV
	 */
	private $wpmudev;

	/**
	 * Hook tag to delete expired files.
	 */
	private const CRON_HOOK = 'wpdef_quarantine_delete_expired';

	/**
	 * Quarantine directory name.
	 */
	private const QUARANTINE_DIRECTORY = '.defender-security-quarantine';

	/**
	 * Quarantine directory permission.
	 */
	public const QUARANTINE_DIRECTORY_PERMISSION = 0755;

	/**
	 * Constructor for the Quarantine component.
	 */
	public function __construct() {
		// Dependencies.
		$this->quarantine_model = wd_di()->get( Quarantine_Model::class );
		$this->scheduler        = wd_di()->get( Scheduler::class );
		$this->scan_setting     = wd_di()->get( Scan_Setting::class );
		$this->main_setting     = wd_di()->get( Main_Setting::class );
		$this->wpmudev          = wd_di()->get( WPMUDEV::class );

		$this->file_expiry_timestamp = $this->set_expiry_timestamp();

		$this->init();
	}

	/**
	 * Get file permission in octal number.
	 *
	 * @param  string $file  File path.
	 *
	 * @return int File permission octal notation.
	 */
	private function get_octet_fileperms( $file ): int {
		clearstatcache();

		return (int) decoct( fileperms( $file ) & 0777 );
	}

	/**
	 * Prepare value for all columns of quarantine table.
	 *
	 * @param  Scan_Item $scan_item  Scan item model object.
	 *
	 * @return Quarantine_Model Quarantine model object.
	 */
	private function prepare_file_metadata( Scan_Item $scan_item ): Quarantine_Model {
		$file = $scan_item->raw_data['file'];

		$file_hash = (string) sha1_file( $file ) . uniqid();

		$path_info = pathinfo( $file );

		$mime = (string) mime_content_type( $file );

		$perms = $this->get_octet_fileperms( $file );

		$file_owner = (string) fileowner( $file );

		$file_group = (string) filegroup( $file );

		$plugin_headers = $this->get_plugin_headers( $file );
		$plugin_headers = reset( $plugin_headers );

		$file_version = isset( $plugin_headers['Version'] ) ? $plugin_headers['Version'] : '';

		$mtime = wp_date( 'Y-m-d H:i:s', (int) filemtime( $file ) );

		$source_slug = $this->get_plugin_directory_name( $file );

		$created_time = wp_date( 'Y-m-d H:i:s' );

		$created_by = get_current_user_id();

		$file_category = $this->quarantine_model::WP_PLUGIN;

		$this->quarantine_model->defender_scan_item_id = $scan_item->id;
		$this->quarantine_model->file_hash             = $file_hash;
		$this->quarantine_model->file_full_path        = $file;
		$this->quarantine_model->file_original_name    = $path_info['filename'];
		$this->quarantine_model->file_extension        = isset( $path_info['extension'] ) ? $path_info['extension'] : '';
		$this->quarantine_model->file_mime_type        = $mime;
		$this->quarantine_model->file_rw_permission    = $perms;
		$this->quarantine_model->file_owner            = $file_owner;
		$this->quarantine_model->file_group            = $file_group;
		$this->quarantine_model->file_version          = $file_version;
		$this->quarantine_model->file_category         = $file_category;
		$this->quarantine_model->file_modified_time    = $mtime;
		$this->quarantine_model->source_slug           = $source_slug;
		$this->quarantine_model->created_time          = $created_time;
		$this->quarantine_model->created_by            = $created_by;

		return $this->quarantine_model;
	}

	/**
	 * Logics to process before file manipulation.
	 *
	 * @param  string $file_path  File path of the plugin.
	 *
	 * @return array Array of plugin data. Keys plugin_basename, is_active & is_network_active.
	 */
	private function before_file_transaction( string $file_path ): array {
		// Plugin active state.
		$plugin_basename   = plugin_basename( $file_path );
		$is_active         = is_plugin_active( $plugin_basename );
		$is_network_active = is_plugin_active_for_network( $plugin_basename );

		// Deactivate plugin which are already activated. No effect on already deactivated plugin.
		if ( $is_active ) {
			deactivate_plugins( $plugin_basename, true, $is_network_active );
		}

		$result = array(
			'plugin_basename'   => $plugin_basename,
			'is_active'         => $is_active,
			'is_network_active' => $is_network_active,
		);

		$this->log_wrapper( $result );

		return $result;
	}

	/**
	 * Logics to process before file manipulation.
	 *
	 * @param  array $data  Data needs to activate the plugin.
	 */
	private function after_file_transaction( array $data ): void {
		$this->log_wrapper( $data );

		// Activate plugin if already activates.
		if ( $data['is_active'] ) {
			activate_plugins( $data['plugin_basename'], '', $data['is_network_active'], true );
		}
	}

	/**
	 * Wrapper method which quarantine the file.
	 * Track plugin activation/deactivation state.
	 * Decide to deactivate plugin based on the state.
	 * Do core quarantine process.
	 * Reinstate previous state of plugin activation/deactivation.
	 *
	 * @param  Scan_Item $scan_item  Scan item model object.
	 * @param  string    $parent_action  Parent action.
	 *
	 * @return array Index message: describes what happened.
	 *               Index success: true if file quarantined and quarantine record
	 *               created else false.
	 */
	public function quarantine_file( Scan_Item $scan_item, string $parent_action ): array {
		$before_file_transaction = $this->before_file_transaction( $scan_item->raw_data['file'] );

		// Do quarantine file processing.
		$action = $this->do_quarantine( $scan_item, $parent_action );

		$this->after_file_transaction( $before_file_transaction );

		$this->wpmudev->schedule_hub_sync();

		return $action;
	}

	/**
	 * Transfer file from source to destination.
	 * Depends on the parent action if action is repair then copy file to destination.
	 * Else move file to destination.
	 *
	 * @param  string $from  Source file path.
	 * @param  string $to  Destination file path.
	 * @param  string $parent_action  Parent action. As of now only two actions repair or delete.
	 *
	 * @return bool True on operation succeeded or False on operation failed.
	 */
	private function file_transfer( string $from, string $to, string $parent_action ): bool {
		global $wp_filesystem;
		if ( 'repair' === $parent_action ) {
			return copy( $from, $to );
		}
		return $wp_filesystem->move( $from, $to, true );
	}

	/**
	 * Core method which Quarantine the file.
	 * Do only file processing and DB handling.
	 *
	 * @param  Scan_Item $scan_item  Scan item model object.
	 * @param  string    $parent_action  Parent action.
	 *
	 * @return array Index message: describes what happened.
	 *               Index success: true if file quarantined and quarantine record
	 *               created else false.
	 */
	private function do_quarantine( Scan_Item $scan_item, string $parent_action ): array {
		$quarantine_model = $this->prepare_file_metadata( $scan_item );

		$this->log_wrapper( $quarantine_model, 'Do quarantine: Prepare file metadata' );
		$this->log_wrapper( $parent_action, 'Do quarantine: Parent action name' );

		$file_name_with_extension = $quarantine_model->file_original_name . '.' . $quarantine_model->file_extension;

		$is_renamed = $this->file_transfer(
			$quarantine_model->file_full_path,
			$this->get_quarantined_file_path( $quarantine_model->file_hash ),
			$parent_action
		);

		if ( ! $is_renamed ) {
			$result = array(
				'message' => sprintf(
					/* translators: 1: Filename with extension */
					esc_html__( 'Failed to quarantine the file %1$s.', 'wpdef' ),
					'<strong>' . $file_name_with_extension . '</strong>'
				),
				'success' => false,
			);

			$this->log_wrapper( $result );

			return $result;
		}

		$saved = $quarantine_model->save();

		if ( ! is_int( $saved ) ) {
			$result = array(
				'message' => sprintf(
					/* translators: 1: Filename with extension */
					esc_html__( 'Failed to add quarantine record in DB for the file %1$s.', 'wpdef' ),
					'<strong>' . $file_name_with_extension . '</strong>'
				),
				'success' => false,
			);

			$this->log_wrapper( $result );

			return $result;
		}

		$message = sprintf(
			/* translators: 1: Filename with extension */
			esc_html__( 'Quarantined the file %1$s and deleted the source file successfully.', 'wpdef' ),
			'<strong>' . $file_name_with_extension . '</strong>'
		);

		if ( 'repair' === $parent_action ) {
			$plugin_write_handler = $this->plugin_write_handler( $quarantine_model );

			if ( false === $plugin_write_handler['success'] ) {
				$this->log_wrapper( $plugin_write_handler, 'Plugin write failed' );

				$this->restore_file( $scan_item );

				return $plugin_write_handler;
			}

			$message = sprintf(
				/* translators: 1: Filename with extension */
				esc_html__(
					'Quarantined the file %1$s and repaired the source file successfully.',
					'wpdef'
				),
				'<strong>' . $file_name_with_extension . '</strong>'
			);
		}

		$result = array(
			'message' => $message,
			'success' => true,
		);

		$scan_item->delete_by_id( $scan_item->id );

		$this->log_wrapper( $result );

		return $result;
	}

	/**
	 * Restore the quarantined file either using Scan_Item object or Quarantine record primary key.
	 *
	 * @param  Scan_Item|int $entity  To determine which file to restore.
	 *
	 * @return array Index message: describes what happened.
	 *               Index success: true if file moved and quarantine record
	 *               removed else false.
	 */
	public function restore_file( $entity ): array {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		if ( $entity instanceof Scan_Item ) {
			$file_metadata = $this->get_record_by_scan_item( $entity );
		} elseif ( is_int( $entity ) ) {
			$file_metadata = $this->quarantine_model->find_by_id( $entity );
		}

		$this->log_wrapper( $file_metadata );

		if (
			! isset(
				$file_metadata,
				$file_metadata->file_hash,
				$file_metadata->file_full_path,
				$file_metadata->id,
				$file_metadata->file_original_name,
				$file_metadata->file_extension
			)
		) {
			$result = array(
				'message' => esc_html__( 'Record not exists in DB.', 'wpdef' ),
				'success' => false,
			);

			$this->log_wrapper( $result );

			return $result;
		}

		$file_name_with_extension = $file_metadata->file_original_name . '.' . $file_metadata->file_extension;

		if ( ! $this->is_quarantine_file_exists( $file_metadata->file_hash ) ) {
			$result = array(
				'message' => esc_html__( 'Quarantined file doesn\'t exist in the quarantine directory.', 'wpdef' ),
				'success' => false,
			);

			$this->log_wrapper(
				$result,
				'External error: File System Error or Quarantined file deleted by someone using OS CMD'
			);

			return $result;
		}

		$plugin_headers = $this->get_plugin_headers( $file_metadata->file_full_path );

		if ( array() === $plugin_headers ) {
			$result = array(
				'message' => sprintf(
					/* translators: 1: Filename with extension */
					esc_html__( 'Plugin main file missing for %1$s file.', 'wpdef' ),
					'<strong>' . $file_name_with_extension . '</strong>'
				),
				'success' => false,
			);

			$this->log_wrapper( $result );
		}

		$quarantined_file_path = $this->get_quarantined_file_path( $file_metadata->file_hash );

		$before_file_transaction = $this->before_file_transaction( $file_metadata->file_full_path );

		$is_renamed = $wp_filesystem->move( $quarantined_file_path, $file_metadata->file_full_path, true );

		$this->after_file_transaction( $before_file_transaction );

		if ( ! $is_renamed ) {
			$result = array(
				'message' => esc_html__( 'Failed to restore quarantined file.', 'wpdef' ),
				'success' => false,
			);

			$this->log_wrapper( $result );

			return $result;
		}

		$is_delete_success = $this->quarantine_model->delete( (int) $file_metadata->id );

		if ( ! $is_delete_success ) {
			$result = array(
				'message' => esc_html__( 'Failed to remove quarantine record from DB.', 'wpdef' ),
				'success' => false,
			);

			$this->log_wrapper( $result );

			return $result;
		}

		$result = array(
			'message' => sprintf(
				/* translators: 1: Filename with extension */
				esc_html__( 'Restored %1$s', 'wpdef' ),
				'<strong>' . $file_name_with_extension . '</strong>'
			),
			'success' => true,
		);

		$this->log_wrapper( $result );

		$this->wpmudev->schedule_hub_sync();

		return $result;
	}

	/**
	 * Return Quarantine model.
	 *
	 * @param  Scan_Item $scan_item  Scan_Item object which individual record of scan data.
	 *
	 * @return mixed Return Quarantine model if exists.
	 */
	private function get_record_by_scan_item( Scan_Item $scan_item ) {
		return $this->quarantine_model->select_restore_detail( $scan_item );
	}

	/**
	 * Check if a file is quarantined.
	 *
	 * @param  Scan_Item $scan_item  The scan item to check.
	 *
	 * @return bool True if the file is quarantined, false otherwise.
	 */
	private function is_quarantined( Scan_Item $scan_item ): bool {
		$quarantined_record = $this->quarantine_model->select_by_file_full_path( $scan_item->raw_data['file'] );

		$is_file_exists = false;

		if ( isset( $quarantined_record[0] ) ) {
			$is_file_exists = $this->is_quarantine_file_exists( $quarantined_record[0]->file_hash );
		}

		return $is_file_exists;
	}

	/**
	 * Get the path to the quarantine directory.
	 *
	 * @return string The path to the quarantine directory.
	 */
	private function get_quarantine_directory(): string {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$quarantine_dir = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . self::QUARANTINE_DIRECTORY;

		if ( ! is_dir( $quarantine_dir ) ) {
			$wp_filesystem->mkdir( $quarantine_dir, self::QUARANTINE_DIRECTORY_PERMISSION );
		}
		$file_helper = wd_di()->get( File_Helper::class );
		$file_helper->maybe_dir_access_deny( $quarantine_dir );

		return $quarantine_dir;
	}

	/**
	 * Get the full path to a quarantined file.
	 *
	 * @param  string $file_hash  The hash of the file.
	 *
	 * @return string The full path to the quarantined file.
	 */
	private function get_quarantined_file_path( string $file_hash ): string {
		return $this->get_quarantine_directory() . DIRECTORY_SEPARATOR . $file_hash . '.restore';
	}

	/**
	 * Writes plugin file by fetching from wp.org repo.
	 *
	 * @param  Quarantine_Model $quarantine_model  SQL ORM object.
	 *
	 * @return array Index message: describes what happened.
	 *               Index success: true if writes successfully
	 *               else false on failed to write or WP plugin repository errors.
	 */
	private function plugin_write_handler( Quarantine_Model $quarantine_model ): array {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$plugin_directory_name = $this->get_plugin_directory_name( $quarantine_model->file_full_path );

		if ( ! $this->is_likely_wporg_slug( $plugin_directory_name ) ) {
			return array(
				'message' => esc_html__( 'Make sure plugin exists in WordPress plugin repository.', 'wpdef' ),
				'success' => false,
			);
		}

		$plugin_relative_path = $this->get_plugin_relative_path( $quarantine_model->file_full_path );
		$is_plugin_in_wp_org  = $this->check_plugin_on_wp_org( $plugin_directory_name );

		$file_name_with_extension = $quarantine_model->file_original_name . '.' . $quarantine_model->file_extension;
		$generic_error            = array(
			'message' => sprintf(
				/* translators: 1: Filename with extension */
				esc_html__( 'Failed to write the file %1$s.', 'wpdef' ),
				'<strong>' . $file_name_with_extension . '</strong>'
			),
			'success' => false,
		);

		if (
			isset( $is_plugin_in_wp_org['success'] ) &&
			true === $is_plugin_in_wp_org['success']
		) {
			$file_url = $this->get_file_url(
				$plugin_directory_name,
				$quarantine_model->file_version,
				$plugin_relative_path
			);

			$file_content = $this->get_url_content( $file_url );

			$is_written = $wp_filesystem->put_contents( $quarantine_model->file_full_path, $file_content );

			if ( $is_written ) {
				return array(
					'message' => sprintf(
						/* translators: 1: Filename with extension */
						esc_html__( 'Writes the file %1$s successfully.', 'wpdef' ),
						'<strong>' . $file_name_with_extension . '</strong>'
					),
					'success' => true,
				);
			}

			return $generic_error;
		} elseif (
			isset( $is_plugin_in_wp_org['success'], $is_plugin_in_wp_org['message'] ) &&
			false === $is_plugin_in_wp_org['success']
		) {
			$error = $is_plugin_in_wp_org['message'];

			return array(
				'message' => sprintf(
					/* translators: 1: WordPress API error message */
					esc_html__( 'WordPress remote error: %1$s', 'wpdef' ),
					'<strong>' . $error . '</strong>'
				),
				'success' => false,
			);
		}

		return $generic_error;
	}

	/**
	 * Check if a quarantined file exists.
	 *
	 * @param  string $file_hash  The hash of the file.
	 *
	 * @return bool True if the file exists, false otherwise.
	 */
	private function is_quarantine_file_exists( string $file_hash ): bool {
		return file_exists( $this->get_quarantined_file_path( $file_hash ) );
	}

	/**
	 * Get a collection of quarantined files.
	 *
	 * @return array An array of quarantined files.
	 */
	public function quarantine_collection(): array {
		$collections = $this->quarantine_model->quarantine_collection();

		foreach ( $collections as $index => $collection ) {
			$name = '';
			// Is the plugin file?
			if ( 0 === strpos( realpath( $collection['file_full_path'] ), realpath( WP_PLUGIN_DIR ) ) ) {
				$plugin_headers = $this->get_plugin_headers( $collection['file_full_path'] );
				if ( is_array( $plugin_headers ) ) {
					$plugin_headers = reset( $plugin_headers );

					$name = isset( $plugin_headers['Name'] ) ? $plugin_headers['Name'] : '';
				}
			}

			$collections[ $index ]['name'] = $name;

			$collections[ $index ]['file_modified_time'] =
				$this->format_date_time( $collection['file_modified_time'] );
			$collections[ $index ]['created_time']       =
				$this->format_date_time( $collection['created_time'] );
		}

		return $collections;
	}

	/**
	 * Hard delete the quarantined file and remove the DB record.
	 *
	 * @param  int $id  Primary key of the record need to be deleted with associated file.
	 *
	 * @return bool True on success else false.
	 */
	public function delete_quarantined_file( int $id ): bool {
		$file_metadata = $this->quarantine_model->find_by_id( $id );

		$this->log_wrapper( $file_metadata, 'On deletion: File metadata' );

		$is_file_exists = $this->is_quarantine_file_exists( $file_metadata->file_hash );

		$this->log_wrapper( 'File exists? ' . $is_file_exists );

		if (
			! is_null( $file_metadata ) &&
			$is_file_exists
		) {
			wp_delete_file(
				$this->get_quarantined_file_path(
					$file_metadata->file_hash
				)
			);
			$is_model_deleted = $this->quarantine_model->delete( $id );

			$this->log_wrapper( 'Model deleted? ' . $is_model_deleted );

			$scan = \WP_Defender\Model\Scan::get_last();
			$scan->remove_related_issue_by( $file_metadata->file_full_path, '' );

			$this->wpmudev->schedule_hub_sync();

			return $is_model_deleted;
		}

		$this->log_wrapper( 'Check SQL row & quarantined file exists for PK: ' . $id );

		return false;
	}

	/**
	 * Delete files which are older the expiry time limit.
	 */
	private function delete_old_file(): void {
		if ( is_int( $this->file_expiry_timestamp ) ) {
			$expiry_limit = wp_date( 'Y-m-d H:i:s', $this->file_expiry_timestamp );

			$old_records = $this->quarantine_model->get_old_records( $expiry_limit );

			foreach ( $old_records as $file_id ) {
				$this->delete_quarantined_file( (int) $file_id['id'] );
			}
		}
	}

	/**
	 * Cron schedule delete old files.
	 */
	public function cron_process(): void {
		$this->delete_old_file();
	}

	/**
	 * Invoke all init methods.
	 */
	public function init(): void {
		$interval = $this->scheduler->get_cron_schedule_interval(
			$this->scan_setting->quarantine_expire_schedule
		);

		if ( false !== $interval ) {
			/**
			 * Delete old quarantined files.
			 *
			 * @var Network_Cron_Manager $network_cron_manager
			 */
			$network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
			$network_cron_manager->register_callback(
				self::CRON_HOOK,
				array( $this, 'cron_process' ),
				$interval
			);
		}

		$this->get_quarantine_directory();
	}

	/**
	 * List of cron schedules utilized by quarantine expiry settings.
	 *
	 * @return array List of WP cron schedules relevant to quarantine period.
	 */
	public function cron_schedules(): array {
		return $this->scheduler->filter_cron_schedules(
			array(
				'thirty_days',
				'sixty_days',
				'ninety_days',
				'six_months',
				'one_year',
			)
		);
	}

	/**
	 * Calculate threshold timestamp.
	 * This timestamp is used to delete sql record created before threshold
	 * timestamp and it's associated file.
	 *
	 * @return ?int Threshold timestamp.
	 */
	private function set_expiry_timestamp(): ?int {
		$schedule_name = $this->scan_setting->quarantine_expire_schedule;

		$get_schedule = $this->scheduler->filter_cron_schedules(
			array( $schedule_name )
		);

		if ( isset( $get_schedule[ $schedule_name ] ) ) {
			$now                        = strtotime( 'now' );
			$now                        = ! is_int( $now ) ? (int) $now : $now;
			$quarantined_time_threshold = $now - $get_schedule[ $schedule_name ]['interval'];

			return $quarantined_time_threshold;
		}

		return null;
	}

	/**
	 * Reschedule the cron.
	 * This method helps to change schedule when expiry period changed and delete
	 * quarantine sql records and associated file if the quarantined time is expired.
	 *
	 * @param  string $prev  Previously selected cron interval.
	 * @param  string $current  Currently selected cron interval.
	 */
	public function reschedule_file_expiry_cron( string $prev, string $current ): void {
		// If prev cron interval and current cron interval same then early return.
		if ( 0 === strcmp( $prev, $current ) ) {
			return;
		}

		// Else if prev cron interval not equal to current cron interval.
		// Replace the previous scheduler with new.
		$this->scheduler->override_schedule( self::CRON_HOOK, $current );

		// Invoke cron process immediately to delete expired file related to current interval.
		$this->cron_process();
	}

	/**
	 * Get quarantine directory URL.
	 *
	 * @return string Quarantine directory URL.
	 */
	public function quarantine_directory_url(): string {
		return content_url( self::QUARANTINE_DIRECTORY );
	}

	/**
	 * Actions to accomplish before plugin uninstallation.
	 * If Remove settings chosen then directly remove data i.e. without archiving the quarantined file and table data.
	 */
	public function on_uninstall(): void {
		$method_name = 'data_' . $this->main_setting->uninstall_quarantine;

		if ( method_exists( $this, $method_name ) ) {
			$this->$method_name();
		}
	}

	/**
	 * Remove quarantine file system & table data.
	 */
	private function data_remove(): void {
		$this->delete_dir( $this->get_quarantine_directory() );
		$this->quarantine_model->drop_table();
	}

	/**
	 * Log wrapper method.
	 *
	 * @param  mixed  $message  Details need to be write accepts string or array or object.
	 * @param  string $custom_trace_title  Description about custom trace event.
	 * @param  bool   $in_depth_debug  If true in depth trace else if false then simple trace.
	 * @param  int    $in_depth_limit  How much deep to trace. Works only for true assigned for $in_depth_debug.
	 * @param  string $category  Log filename.
	 */
	private function log_wrapper(
		$message,
		string $custom_trace_title = '',
		bool $in_depth_debug = false,
		int $in_depth_limit = 1,
		string $category = 'quarantine'
	): void {
		$this->log( 'Backtrace Summary:', $category );
		/**
		 * Ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
		 * Why?
		 * This is an internal function and it's using to debug error in production.
		 */
		$this->log( wp_debug_backtrace_summary( null, 1, false ), $category ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary

		$this->log( 'Custom Trace:', $category );

		if ( '' !== $custom_trace_title ) {
			$this->log( $custom_trace_title, $category );
		}

		$this->log( $message, $category );

		if ( true === $in_depth_debug ) {
			$this->log( 'Detailed Debug:', $category );
			/**
			 * Ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
			 * Why?
			 * This is an internal function and it's using to debug error in production.
			 */
			$this->log( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $in_depth_limit ), $category ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
		}
	}

	/**
	 * Data required from quarantine on scan item.
	 *
	 * @param  Scan_Item $scan_item  Scan item model object.
	 *
	 * @return array Array of data required from quarantine on scan item.
	 */
	public function scan_item_data( Scan_Item $scan_item ): array {
		return array(
			'is_quarantined'   => $this->is_quarantined( $scan_item ),
			'is_quarantinable' => $this->is_quarantinable( $scan_item->raw_data['file'] ),
		);
	}

	/**
	 * Array of quarantined files, and it's source file details for HUB widget listing.
	 *
	 * @return array Quarantined files details.
	 */
	public function hub_list(): array {
		$model_list = $this->quarantine_model->hub_list();

		$hub_api_list = array();

		foreach ( $model_list as $quarantine_item ) {
			$id               = $quarantine_item['id'];
			$file_name        = $quarantine_item['file_original_name'] . '.' . $quarantine_item['file_extension'];
			$quarantined_time = strtotime( $quarantine_item['quarantined_time'] );
			$quarantined_path = $this->get_quarantined_file_path( $quarantine_item['quarantined_path'] );
			$source_path      = $quarantine_item['source_path'];

			array_push(
				$hub_api_list,
				compact(
					'id',
					'file_name',
					'quarantined_time',
					'quarantined_path',
					'source_path'
				)
			);
		}

		return $hub_api_list;
	}

	/**
	 * Check the quarantine directory is forbidden.
	 * When user visits the URL of the quarantine directory it should not be available.
	 * This function returns false if we get the 200 status code and true for any other status codes.
	 *
	 * @return bool
	 */
	public function is_quarantine_directory_url_forbidden(): bool {
		$response = wp_remote_head(
			$this->quarantine_directory_url(),
			array( 'timeout' => 5 )
		);

		$is_200 = 200 === (int) wp_remote_retrieve_response_code( $response );

		return ! $is_200;
	}
}