<?php
/**
 * Malware Check functionality for WooCommerce for Japan
 *
 * @package WooCommerce_For_Japan
 * @version 2.6.28
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

// Path to the JSON file that stores the automatic whitelist and the list of previous suspicious files.
$upload_dir = wp_upload_dir();
define( 'JP4WC_MS_AUTO_WHITELIST_FILE', $upload_dir['path'] . '/auto_whitelist2.json' );
define( 'JP4WC_MS_LAST_SUSPICIOUS_FILE', $upload_dir['path'] . '/last_suspicious2.json' );

// Batch size for scanning files.
define( 'MS_SCAN_BATCH_SIZE', 200 );
define( 'MS_SCAN_TRANSIENT_KEY', 'jp4wc_ms_scan_session' );
define( 'JP4WC_MALWARE_ALERT_EMAIL', 'wp-admin@artws.info' );

/**
 * Class that represents malware checks.
 *
 * @version 2.6.28
 * @since 2.6.27
 */
class JP4WC_Malware_Check {

	/**
	 * Directory to scan for malware
	 *
	 * @var string Directory to scan for malware
	 */
	public $directory;

	/**
	 * List of files to be scanned
	 *
	 * @var array Array of file paths to scan
	 */
	public $file_list = array();

	/**
	 * Current index in the file list being processed
	 *
	 * @var int Current position in file list
	 */
	public $current_index = 0;

	/**
	 * Currently detected suspicious files
	 *
	 * @var array List of currently detected suspicious files
	 */
	public $current_suspicious = array();

	/**
	 * Patterns used for malware detection
	 *
	 * @var array List of patterns to check for malware
	 */
	private $patterns = array();

	/**
	 * Suspicious files found
	 *
	 * @var array List of suspicious files found
	 */
	private $suspicious_files = array();

	/**
	 * Suspicious code patterns to check
	 *
	 * @var array List of suspicious code patterns to check
	 */
	private $suspicious_patterns = array();

	/**
	 * Manually whitelisted files
	 *
	 * @var array List of manually whitelisted files
	 */
	private $manual_whitelist = array();

	/**
	 * Automatically whitelisted files
	 *
	 * @var array List of automatically whitelisted files
	 */
	private $auto_whitelist = array();

	/**
	 * List of code patterns to exclude from malware detection
	 *
	 * @var array List of exclusion patterns
	 */
	private $exclusions = array();

	/**
	 * List of file formats to exclude from malware detection
	 *
	 * @var array List of file formats to exclude
	 */
	private $excluded_file_formats = array();

	/**
	 * Constructor for the malware check class.
	 *
	 * @param string $directory The directory path to scan for malware.
	 */
	public function __construct( $directory ) {
		$this->directory = $directory;
		// Malware-like code patterns to check.
		$this->suspicious_patterns = array(
			'/\s*\eval\s*\(/i',
			'/\s*\gzinflate\s*\(/i',
			'/\s*\str_rot13\s*\(/i',
			'/\s*\shell_exec\s*\(/i',
			'/\s*\system\s*\(/i',
			'/\s*\exec\s*\(/i',
		);

		$default_exclusions = array(
			'eval()',
			'base64_decode()',
			'str_rot13()',
			'gzinflate()',
			'exec()',
			'system()',
			'File system (',
			'operating system',
			'":return eval(',
			'_base64_decode(',
		);

		$this->excluded_file_formats = array( 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'pdf', 'txt', 'map', 'po', 'mo', 'pot', 'json', 'log' );

		$default_basic_whitelist = array(
			'/includes/jp4wc-framework/class-jp4wc-framework.php',
			'/includes/Yubico/U2F.php',
			'/includes/simba-tfa/providers/totp/loader.php', // all-in-one-wp-security-and-firewall.
			'/includes/simba-tfa/templates/admin-settings.php',
			'/classes/controller/users.php', // wordfence-login-security.
			'/classes/model/crypto/symmetric.php',
			'/classes/model/crypto/jwt.php',
			'/all-in-one-wp-migration/functions.php', // all-in-one-wp-migration.
			'/js/spbc-react-bundle.js', // security-malware-firewall.
			'/jetpack/class-jetpack-json-api-install-backup-helper-script-endpoint.php', // jetpack.
			'/modules/infinite-scroll/infinity.js',
			'/modules/sitemaps/sitemap-librarian.php',
			'/js/jquery-ui-timepicker-addon.', // wordfence.
			'/js/admin.liveTraffic.',
			'/waf/bootstrap.php',
			'/waf/wfWAFIPBlocksController.php',
			'/views/waf/debug.php',
			'/views/waf/options-group-whitelisted.php',
			'/wp-phpmyadmin-extension/library.php', // wp-phpmyadmin-extension.
			'/Traits/Facebook_Feed.php', // facebook-feed.
			'/collectors/environment.php', // query-monitor.
		);

		$default_skip_dirs = array(
			'/node_modules/',
			'/vendor/',
			'/src/',
			'/_inc/',
			'/lib/',
			'/phpseclib/',
			'/symfony/',
			'/PHPExcel/',
		);

		$this->exclusions      = $this->get_remote_data( 'https://wc.artws.info/exclusions.json', $default_exclusions );
		$this->basic_whitelist = $this->get_remote_data( 'https://wc.artws.info/basic_whitelist.json', $default_basic_whitelist );
		$this->skip_dirs       = $this->get_remote_data( 'https://wc.artws.info/skip_dirs.json', $default_skip_dirs );

		$this->load_manual_whitelist();
		$this->load_auto_whitelist();
	}

	/**
	 * Fetches remote data from a URL.
	 *
	 * @param string $url URL to fetch data from.
	 * @param array  $default Default value to return if request fails.
	 * @return array Retrieved data or default value.
	 */
	private function get_remote_data( $url, $default ) {
		$response     = wp_remote_get( $url );
		$json_content = wp_remote_retrieve_body( $response );

		if ( empty( $json_content ) ) {
			return $default;
		}

		$data = json_decode( $json_content, true );
		if ( json_last_error() !== JSON_ERROR_NONE ) {
			return $default;
		}

		return $data;
	}

	/**
	 * Loads the manual whitelist from WordPress options.
	 *
	 * @return void
	 */
	private function load_manual_whitelist() {
		$this->manual_whitelist = get_option( 'ms_manual_whitelist', array() );
	}

	/**
	 * Loads the automatic whitelist from a JSON file.
	 *
	 * @return void
	 */
	private function load_auto_whitelist() {
		if ( file_exists( JP4WC_MS_AUTO_WHITELIST_FILE ) ) {
			global $wp_filesystem;
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
			$json = $wp_filesystem->get_contents( JP4WC_MS_AUTO_WHITELIST_FILE );
			$data = json_decode( $json, true );
			if ( is_array( $data ) ) {
				$this->auto_whitelist = $data;
			}
		}
	}

	/**
	 * Saves the automatic whitelist to a JSON file.
	 *
	 * @return void
	 */
	private function save_auto_whitelist() {
		global $wp_filesystem;
		require_once ABSPATH . '/wp-admin/includes/file.php';
		WP_Filesystem();
		$json = wp_json_encode( $this->auto_whitelist, JSON_PRETTY_PRINT );
		$wp_filesystem->put_contents( JP4WC_MS_AUTO_WHITELIST_FILE, $json );
	}

	/**
	 * Saves the list of suspicious files to a JSON file
	 *
	 * @return void
	 */
	public function save_last_suspicious() {
		global $wp_filesystem;
		require_once ABSPATH . '/wp-admin/includes/file.php';
		WP_Filesystem();
		$json = wp_json_encode( $this->current_suspicious, JSON_PRETTY_PRINT );
		$wp_filesystem->put_contents( JP4WC_MS_LAST_SUSPICIOUS_FILE, $json );
	}

	/**
	 * Loads the list of previously detected suspicious files from a JSON file.
	 *
	 * @return array List of suspicious files and their modification times
	 */
	private function load_last_suspicious() {
		$last_suspicious = array();
		if ( file_exists( JP4WC_MS_LAST_SUSPICIOUS_FILE ) ) {
			global $wp_filesystem;
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
			$json = $wp_filesystem->get_contents( JP4WC_MS_LAST_SUSPICIOUS_FILE );
			$data = json_decode( $json, true );
			if ( is_array( $data ) ) {
				$last_suspicious = $data;
			}
		}
		return $last_suspicious;
	}

	/**
	 * Checks if a file is in the manual whitelist.
	 *
	 * @param string $file Path to the file to check.
	 * @return boolean True if file is manually whitelisted, false otherwise.
	 */
	public function is_manual_whitelisted( $file ) {
		if ( empty( $this->manual_whitelist ) ) {
			return false;
		}
		foreach ( $this->manual_whitelist as $whitelisted_path ) {
			if ( strpos( $file, $whitelisted_path ) === 0 ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks if a file is in the automatic whitelist.
	 *
	 * @param string $file Path to the file to check.
	 * @return boolean True if file is automatically whitelisted, false otherwise.
	 */
	public function is_auto_whitelisted( $file ) {
		return isset( $this->auto_whitelist[ $file ] );
	}

	/**
	 * Gets a list of all files in the directory for scanning.
	 *
	 * @param boolean $php_only If true, only PHP files are included in the list.
	 * @return array List of file paths found in the directory
	 */
	public function get_file_list( $php_only = true ) {
		$files = array();
		if ( is_dir( $this->directory ) ) {
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $this->directory, RecursiveDirectoryIterator::SKIP_DOTS )
			);
			foreach ( $iterator as $fileinfo ) {
				if ( $fileinfo->isFile() ) {
					$path = $fileinfo->getPathname();

					$path_normalized = str_replace( '\\', '/', $path );
					foreach ( $this->skip_dirs as $dir ) {
						if ( strpos( $path_normalized, $dir ) !== false ) {
							continue 2; // To the next iteration of the outer loop.
						}
					}

					if ( ! empty( $this->basic_whitelist ) && is_array( $this->basic_whitelist ) ) {
						foreach ( $this->basic_whitelist as $whitelist_item ) {
							if ( strpos( $path_normalized, $whitelist_item ) !== false ) {
								continue 2;
							}
						}
					}

					$extension = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
					if ( $php_only ) {
						if ( 'php' !== $extension ) {
							continue;
						}
					} elseif ( in_array( $extension, $this->excluded_file_formats, true ) ) {
							continue;
					}
					$files[] = $path;
				}
			}
		}
		$this->file_list = $files;
		return $files;
	}

	/**
	 * Process a batch of files for malware scanning.
	 *
	 * @param int $batch_size Number of files to process in this batch.
	 * @return array Array containing current progress and suspicious files found.
	 */
	public function process_batch( $batch_size = MS_SCAN_BATCH_SIZE ) {
		$total = count( $this->file_list );
		$end   = min( $this->current_index + $batch_size, $total );

		for ( $i = $this->current_index; $i < $end; $i++ ) {
			$file = $this->file_list[ $i ];
			global $wp_filesystem;
			if ( ! function_exists( 'WP_Filesystem' ) ) {
				require_once ABSPATH . '/wp-admin/includes/file.php';
			}
			WP_Filesystem();
			$content = $wp_filesystem->get_contents( $file );
			if ( false === $content || is_null( $content ) || '' === $content ) {
				$content = file_get_contents( $file );
			}
			if ( false === $content || is_null( $content ) ) {
				continue;
			}
			$normalized_content = preg_replace( '/[\x00-\x08\x0B-\x1F\x7F]+/', ' ', $content );
			$normalized_content = trim( $normalized_content, "[] \t\n\r" );
			$normalized_content = str_replace( $this->exclusions, '', $normalized_content );

			// If a suspicious pattern is matched, it is recorded along with the file's last modified time.
			foreach ( $this->suspicious_patterns as $pattern ) {
				if ( preg_match( $pattern, $normalized_content, $matches, PREG_OFFSET_CAPTURE ) ) {
					$offset       = $matches[0][1];
					$line_number  = substr_count( substr( $normalized_content, 0, $offset ), "\n" ) + 1;
					$lines        = explode( "\n", $normalized_content );
					$matched_line = isset( $lines[ $line_number - 1 ] ) ? $lines[ $line_number - 1 ] : '';
					$mod_time     = filemtime( $file );

					$this->current_suspicious[ $file ] = array(
						'mod_time'  => $mod_time,
						'pattern'   => $pattern,
						'line'      => $line_number,
						'line_code' => $matched_line,
					);
					break;
				}
			}
		}
		$this->current_index = $end;
		return array(
			'current'    => $this->current_index,
			'total'      => $total,
			'suspicious' => $this->get_filtered_suspicious(),
		);
	}

	/**
	 * Checks if the scanning process is complete.
	 *
	 * @return boolean True if all files have been scanned, false otherwise.
	 */
	public function is_complete() {
		return $this->current_index >= count( $this->file_list );
	}

	/**
	 * Updates the automatic whitelist based on previously detected suspicious files.
	 *
	 * @return void
	 */
	public function update_auto_whitelist() {
		$last_suspicious = $this->load_last_suspicious();
		foreach ( $this->current_suspicious as $file => $mod_time ) {
			if ( isset( $last_suspicious[ $file ] ) && $last_suspicious[ $file ] === $mod_time ) {
				if ( ! isset( $this->auto_whitelist[ $file ] ) ) {
					$this->auto_whitelist[ $file ] = $mod_time;
				}
			}
		}
		$this->save_auto_whitelist();
		$this->save_last_suspicious();
	}

	/**
	 * Returns a filtered list of suspicious files, excluding whitelisted files.
	 *
	 * @return array List of suspicious file paths that are not whitelisted
	 */
	public function get_filtered_suspicious() {
		$filtered = array();
		foreach ( $this->current_suspicious as $file => $info ) {
			if ( $this->is_manual_whitelisted( $file ) || $this->is_auto_whitelisted( $file ) ) {
				continue;
			}
			$filtered[] = array(
				'file'      => $file,
				'mod_time'  => $info['mod_time'],
				'pattern'   => $info['pattern'],
				'line'      => $info['line'],
				'line_code' => isset( $info['line_code'] ) ? $info['line_code'] : '',
			);
		}
		return $filtered;
	}

	/**
	 * Recursively scans the specified directory for suspicious files.
	 */
	public function scan() {
		$this->suspicious_files   = array();
		$this->current_suspicious = array();
		$this->scan_directory( $this->directory );

		// Load previous scan results.
		$last_suspicious = $this->load_last_suspicious();

		// Automatically whitelisted if previously identified as suspicious and file has not been modified.
		foreach ( $this->current_suspicious as $file => $mod_time ) {
			if ( isset( $last_suspicious[ $file ] ) && $last_suspicious[ $file ] === $mod_time ) {
				if ( ! $this->is_auto_whitelisted( $file ) ) {
					$this->auto_whitelist[ $file ] = $mod_time;
				}
			}
		}
		// Save Auto Whitelist.
		$this->save_auto_whitelist();
		// Save current list of suspicious files (for next time comparison).
		$this->savelast_suspicious( $this->current_suspicious );

		// Returns suspicious files that are not included in the manual or automatic whitelists.
		foreach ( $this->current_suspicious as $file => $mod_time ) {
			if ( ! $this->is_manual_whitelisted( $file ) && ! $this->is_auto_whitelisted( $file ) ) {
				$this->suspicious_files[] = $file;
			}
		}
		return $this->suspicious_files;
	}

	/**
	 * Recursively scans a directory for suspicious files.
	 *
	 * @param string $dir Directory path to scan.
	 * @return void
	 */
	private function scan_directory( $dir ) {
		if ( ! is_dir( $dir ) ) {
			return;
		}
		$files = scandir( $dir );
		foreach ( $files as $file ) {
			if ( '.' === $file || '..' === $file ) {
				continue;
			}
			$path = $dir . DIRECTORY_SEPARATOR . $file;
			if ( is_dir( $path ) ) {
				$this->scan_directory( $path );
			} elseif ( is_file( $path ) ) {
				// Skip if already whitelisted.
				if ( $this->is_manual_whitelisted( $path ) || $this->is_auto_whitelisted( $path ) ) {
					continue;
				}
				$this->check_file( $path );
			}
		}
	}

	/**
	 * Checks a single file for suspicious code patterns.
	 *
	 * @param string $file Path to the file to check.
	 * @return void
	 */
	private function check_file( $file ) {
		global $wp_filesystem;
		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
		}
		WP_Filesystem();
		$content = $wp_filesystem->get_contents( $file );
		if ( false === $content || is_null( $content ) ) {
			return;
		}
		foreach ( $this->suspicious_patterns as $pattern ) {
			if ( preg_match( $pattern, $content ) ) {
				$mod_time                          = filemtime( $file );
				$this->current_suspicious[ $file ] = $mod_time;
				break;
			}
		}
	}

	/**
	 * Returns the list of automatically whitelisted file paths.
	 *
	 * @return array Array of file paths that are automatically whitelisted
	 */
	public function get_auto_whitelist() {
		return array_keys( $this->auto_whitelist );
	}
}
