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

namespace WP_Defender\Behavior\Scan;

use stdClass;
use WP_Defender\Traits\IO;
use PHP_CodeSniffer\Config;
use Calotes\Component\Behavior;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Tokenizers\PHP;
use PHP_CodeSniffer\Util\Standards;
use WP_Defender\Component\Scan\Tokens;
use PHP_CodeSniffer\Tokenizers\Comment;
use PHP_CodeSniffer\Tokenizers\Tokenizer;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Exceptions\TokenizerException;

/**
 * We need to verify the result to avoid false positive.
 */
class Malware_Deep_Scan extends Behavior {

	use IO;

	/**
	 * Perform a deep scan on the provided file content to identify specific tokens.
	 *
	 * @param  mixed $file  The file content to be scanned.
	 * @param  array $rules  The rules to be applied during scanning.
	 * @param  array $quick_found  Array containing quick found items.
	 *
	 * @return array|bool Returns the array of quick found items after scanning or false if no tokens are found.
	 */
	public function do_deep_scan( $file, $rules, $quick_found ) {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$this->load_phpcodesniffer_needed();
		if ( ! defined( 'PHP_CODESNIFFER_VERBOSITY' ) ) {
			define( 'PHP_CODESNIFFER_VERBOSITY', 0 );
		}
		if ( ! defined( 'PHP_CODESNIFFER_CBF' ) ) {
			define( 'PHP_CODESNIFFER_CBF', false );
		}
		$content = $wp_filesystem->get_contents( $file );
		try {
			$parser         = new PHP( $content, $this->make_phpsniff_config(), PHP_EOL );
			Tokens::$tokens = $parser->getTokens();
			foreach ( $quick_found as &$item ) {
				if ( ! isset( $item['catches'] ) || ! is_array( $item['catches'] ) ) {
					continue;
				}
				foreach ( $item['catches'] as &$catch ) {
					$offset          = $catch['offset'];
					$catch['line']   = Tokens::get_line_from_offset( $content, $offset );
					$catch['mapper'] = Tokens::get_offsets_map( Tokens::get_tokens_by_line( $catch['line'] ) );
					/**
					 * Todo: uncomment the following lines if you need to get details of the code.
					 * $code = Tokens::formatter( Tokens::get_tokens_by_line( $catch['line'] ) );
					 * $catch['code'] = $code;
					 */
					$tokens = Tokens::get_tokens_by_line( $catch['line'] );
					foreach ( $tokens as $k => $token ) {
						if ( in_array(
							$token['code'],
							array(
								T_DOC_COMMENT_WHITESPACE,
								T_DOC_COMMENT_STAR,
								T_DOC_COMMENT_WHITESPACE,
								T_DOC_COMMENT_STRING,
							),
							true
						) ) {
							unset( $tokens[ $k ] );
						}
					}
					$tokens = array_filter( $tokens );
					if ( empty( $tokens ) ) {
						// This mean the catch is comment, do nothing.
						return false;
					}
					unset( $catch['text'] );
				}
			}

			return $quick_found;
		} catch ( TokenizerException $e ) {
			$this->log( $e->getMessage() );
		}

		return false;
	}

	/**
	 * Load PHP CodeSniffer components if needed.
	 */
	private function load_phpcodesniffer_needed() {
		$ds          = DIRECTORY_SEPARATOR;
		$vendor_path = defender_path( 'src/extra/php-codesniffer-php-token' );
		if ( ! class_exists( \PHP_CodeSniffer\Util\Tokens::class ) ) {
			require_once $vendor_path . $ds . 'Util' . $ds . 'Tokens.php';
		}
		if ( ! class_exists( Common::class ) ) {
			require_once $vendor_path . $ds . 'Util' . $ds . 'Common.php';
		}

		if ( ! class_exists( Tokenizer::class ) ) {
			require_once $vendor_path . $ds . 'Tokenizers' . $ds . 'Tokenizer.php';
		}

		if ( ! class_exists( PHP::class ) ) {
			require_once $vendor_path . $ds . 'Tokenizers' . $ds . 'PHP.php';
		}

		if ( ! class_exists( Comment::class ) ) {
			require_once $vendor_path . $ds . 'Tokenizers' . $ds . 'Comment.php';
		}

		if ( ! class_exists( Standards::class ) ) {
			require_once $vendor_path . $ds . 'Util' . $ds . 'Standards.php';
		}

		if ( ! class_exists( RuntimeException::class ) ) {
			require_once $vendor_path . $ds . 'Exceptions' . $ds . 'RuntimeException.php';
		}

		if ( ! class_exists( DeepExitException::class ) ) {
			require_once $vendor_path . $ds . 'Exceptions' . $ds . 'DeepExitException.php';
		}

		if ( ! class_exists( Config::class ) ) {
			require_once $vendor_path . $ds . 'Config.php';
		}
	}

	/**
	 * Create a new PHP CodeSniffer configuration object with default values.
	 *
	 * @return stdClass Returns the PHP CodeSniffer configuration object with default values set.
	 */
	private function make_phpsniff_config() {
		// This config will be used by PHP_CodeSniffer. So we can ignore snake_case naming convention.
		// @codingStandardsIgnoreStart
		$config                  = new stdClass();
		$config->files           = array();
		$config->standards       = array( 'PEAR' );
		$config->verbosity       = 0;
		$config->interactive     = false;
		$config->cache           = false;
		$config->cacheFile       = null;
		$config->colors          = false;
		$config->explain         = false;
		$config->local           = false;
		$config->showSources     = true;
		$config->showProgress    = false;
		$config->quiet           = false;
		$config->annotations     = true;
		$config->parallel        = 1;
		$config->tabWidth        = 0;
		$config->encoding        = 'utf-8';
		$config->extensions      = array(
			'php' => 'PHP',
			'inc' => 'PHP',
			'js'  => 'JS',
			'css' => 'CSS',
		);
		$config->sniffs          = array();
		$config->exclude         = array();
		$config->ignored         = array();
		$config->reportFile      = null;
		$config->generator       = null;
		$config->filter          = null;
		$config->bootstrap       = array();
		$config->basepath        = null;
		$config->reports         = array( 'full' => null );
		$config->reportWidth     = 'auto';
		$config->errorSeverity   = 5;
		$config->warningSeverity = 5;
		$config->recordErrors    = true;
		$config->suffix          = '';
		$config->stdin           = true;
		$config->stdinContent    = null;
		$config->stdinPath       = null;
		$config->unknown         = array();
		// @codingStandardsIgnoreEnd
		return $config;
	}
}