<?php
/**
 * Handles malware scan.
 *
 * @package WP_Defender\Behavior\Scan
 */

namespace WP_Defender\Behavior\Scan;

use ArrayIterator;
use WP_Defender\Traits\IO;
use Calotes\Base\Component;
use WP_Defender\Traits\Plugin;
use WP_Defender\Component\Timer;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Model\Scan as Model_Scan;
use WP_Defender\Model\Setting\Scan as Scan_Settings;
use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
use WP_Defender\Controller\Scan as Scan_Controller;
use WP_Defender\Component\Scan as Scan_Component;

/**
 * It is responsible for performing suspicious checks on files using a quick scan and a deep scan.
 */
class Malware_Scan extends Component {

	use IO;
	use Plugin;

	public const YARA_RULES  = 'defender_yara_rules';
	public const MALWARE_LOG = 'malware_scan.log';

	/**
	 * Backup memory.
	 *
	 * @var string
	 */
	private $memory;

	/**
	 * Holds the WPMUDEV object or null if not set.
	 *
	 * @var WPMUDEV
	 */
	private $wpmudev;

	/**
	 * Holds the Model_Scan model or null if not set.
	 *
	 * @var Model_Scan
	 */
	private $scan;

	/**
	 * Constructor for the Malware_Scan class.
	 *
	 * @param  WPMUDEV    $wpmudev  The WPMUDEV object.
	 * @param  Model_Scan $scan  The Model_Scan model.
	 */
	public function __construct( WPMUDEV $wpmudev, Model_Scan $scan ) {
		$this->wpmudev = $wpmudev;
		$this->scan    = $scan;
	}

	/**
	 * Retrieves additional rules based on the provided scan settings.
	 *
	 * @param  Scan_Settings $scan_settings  The scan settings object.
	 *
	 * @return array Returns an array containing plugin-related rules.
	 */
	protected function get_additional_rules( Scan_Settings $scan_settings ): array {
		$plugin_cache         = false;
		$plugin_slugs_changes = array();
		$plugin_pro_slugs     = array();
		$plugin_all_slugs     = array();
		// Checked Plugin option.
		if ( $scan_settings->integrity_check && $scan_settings->check_plugins ) {
			$plugin_cache = true;
			$arr          = get_site_option( Plugin_Integrity::PLUGIN_SLUGS, array() );
			if ( is_array( $arr ) && array() !== $arr ) {
				$plugin_slugs_changes = $arr;
			}
			$plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, array() );
			if ( is_array( $plugin_slugs ) && array() !== $plugin_slugs ) {
				$plugin_pro_slugs = $plugin_slugs;
			}
			$actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
			$plugin_all_slugs = Scan_Component::are_actioned_plugins( $actioned_plugins )
				? array_keys( $actioned_plugins )
				: $this->get_plugin_slugs();
		}

		return array(
			'plugin_change'        => $plugin_cache,
			'plugin_all_slugs'     => $plugin_all_slugs,
			// List of plugins with modifications.
			'plugin_slugs_changes' => $plugin_slugs_changes,
			// List of pro plugins.
			'plugin_pro_slugs'     => $plugin_pro_slugs,
		);
	}

	/**
	 * Check if a file has been modified based on the given rules.
	 *
	 * @param  string $file_path  The path of the file to check.
	 * @param  array  $rules  The rules to determine if a file has been modified.
	 *
	 * @return bool Returns true if the file has been modified, false otherwise.
	 */
	protected function was_modificated_file( $file_path, $rules ): bool {
		// Unchecked 'Plugin change file' option, so green light to display Suspicious checks.
		if ( ! $rules['plugin_change'] ) {
			return true;
		}

		$search_on_plugin = WP_PLUGIN_DIR . '/';
		if ( false !== stripos( $file_path, $search_on_plugin ) ) {
			/**
			 * Suspicious code in /plugins.
			 * Empty list of plugin slugs because there are premium plugins,
			 * not modifications on Free plugins,
			 * or not plugins on site.
			 * Should check separate custom files/dirs in the root too.
			 */
			$rev_file  = str_replace( $search_on_plugin, '', $file_path );
			$matches   = explode( '/', $rev_file );
			$base_slug = array_shift( $matches );
			// Custom files/dirs.
			if ( ! in_array( $base_slug, $rules['plugin_all_slugs'], true ) ) {
				return true;
			}
			if (
				( ! isset( $rules['plugin_slugs_changes'] ) || array() === (array) $rules['plugin_slugs_changes'] )
				&& ( ! isset( $rules['plugin_pro_slugs'] ) || array() === (array) $rules['plugin_pro_slugs'] )
			) {
				// No modifications.
				return false;
			}
			// Is it on premium plugins?
			if ( in_array( $base_slug, $rules['plugin_pro_slugs'], true ) ) {
				return true;
			}
			if ( in_array( $base_slug, (array) $rules['plugin_slugs_changes'], true ) ) {
				// Modifications in this plugin.
				return true;
			}

			// Modifications are not here.
			return false;
		}

		// Other WP places.
		return true;
	}

	/**
	 * Perform a suspicious check on files using a quick scan and a deep scan.
	 *
	 * @param  Malware_Quick_Scan $quick_scan  The quick scan object.
	 * @param  Malware_Deep_Scan  $deep_scan  The deep scan object.
	 *
	 * @return bool Returns true if the check is successful, false otherwise.
	 */
	public function suspicious_check( Malware_Quick_Scan $quick_scan, Malware_Deep_Scan $deep_scan ): bool {
		$files = get_site_option( Gather_Fact::CACHE_CONTENT, array() );
		if ( array() === $files ) {
			return true;
		}

		set_time_limit( 0 );
		$this->prepare_emergency_shutdown();
		$timer = new Timer();
		$rules = $this->fetch_yara_rules();

		$model = $this->scan;
		$pos   = (int) $model->task_checkpoint;

		$combinations = $this->get_additional_rules( new Scan_Settings() );
		$files        = new ArrayIterator( $files );
		$files->seek( $pos );
		while ( $files->valid() ) {
			if ( ! $timer->check() ) {
				$reason = 'Rage quit';

				/**
				 * Retrieves the Scan_Analytics class.
				 *
				 * @var Scan_Analytics $scan_analytics
				 */
				$scan_analytics = wd_di()->get( Scan_Analytics::class );

				$scan_analytics->track_feature(
					$scan_analytics::EVENT_SCAN_FAILED,
					array(
						$scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
						'Error_Reason' => $reason,
					)
				);

				$this->log( $reason, self::MALWARE_LOG );
				$model->save();
				break;
			}
			if ( $model->is_issue_ignored( $files->current() ) ) {
				$this->log( sprintf( 'skip %s because of file is ignored', $files->current() ), self::MALWARE_LOG );
				$files->next();
				continue;
			}

			[ $result, $qs_detail ] = $quick_scan->do_quick_scan( $files->current(), $rules );
			if ( $result ) {
				$this->log( sprintf( 'file %s suspicious', $files->current() ), self::MALWARE_LOG );
				$result = $deep_scan->do_deep_scan( $files->current(), $qs_detail );
				/**
				 * Add new item if Suspicious code is found and:
				 * plugins are premium,
				 * plugins are on wp.org but the code doesn't match from the WP repo (there are differences in checksums),
				 * deactivated options of File change detection > Scan plugin file changes.
				 */
				if ( is_array( $result ) && $this->was_modificated_file( $files->current(), $combinations ) ) {
					$result['file'] = $files->current();
					$model->add_item( Scan_Item::TYPE_SUSPICIOUS, $result );
				}
			}
			$files->next();
			$files_key              = $files->key();
			$model->task_checkpoint = ! is_null( $files_key ) ? $files_key : '';
			$model->calculate_percent( $files_key * 100 / $files->count(), 6 );
			if ( 0 === $files_key % 100 ) {
				// We should update the model percent each 100 files so we have some progress on the screen.
				$model->save();
			}
		}

		if ( ! $files->valid() ) {
			$last = Model_Scan::get_last();
			if ( is_object( $last ) ) {
				$ignored_issues = $last->get_issues( Scan_Item::TYPE_SUSPICIOUS, Scan_Item::STATUS_IGNORE );
				foreach ( $ignored_issues as $issue ) {
					$this->scan->add_item(
						Scan_Item::TYPE_SUSPICIOUS,
						$issue->raw_data,
						Scan_Item::STATUS_IGNORE
					);
				}
			}
			$model->task_checkpoint = '';
		}

		$model->save();

		return ! $files->valid();
	}

	/**
	 * We will use this for a safe switch when memory out happen.
	 */
	public function prepare_emergency_shutdown() {
		$this->memory = str_repeat( '*', 1024 * 1024 );

		register_shutdown_function(
			function () {
				if ( Model_Scan::STATUS_FINISH === $this->scan->status ) {
					return;
				}

				$this->memory = null;
				$err          = error_get_last();
				if (
					( ! is_null( $err ) )
					&& ( ! in_array( $err['type'], array( E_NOTICE, E_WARNING, E_DEPRECATED ), true ) )
				) {
					$this->log( $err, Scan_Controller::SCAN_LOG );
					$this->log( 'Something wrong happen, saving and quit.', Scan_Controller::SCAN_LOG );
					$this->scan->status = Model_Scan::STATUS_ERROR;
					++$this->scan->task_checkpoint;
					$this->scan->save();
				}
			}
		);
	}

	/**
	 * Fetch yara rules from API.
	 *
	 * @return array
	 */
	private function fetch_yara_rules(): array {
		$rules = get_site_option( self::YARA_RULES, false );
		if ( is_array( $rules ) ) {
			return $rules;
		}

		$rules = $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_SIGNATURE );
		if ( is_array( $rules ) ) {
			update_site_option( self::YARA_RULES, $rules );

			return $rules;
		}

		return array();
	}
}
