<?php
/**
 * Malware Scanner utility: pattern loading, progress transient, advanced whitelist helpers.
 *
 * @package WPSecurityNinja\Plugin
 */

namespace WPSecurityNinja\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Wf_Sn_Ms_Util
 */
class Wf_Sn_Ms_Util {

	const TRANSIENT_PROGRESS = 'wf_sn_ms_scan_progress';
	const TRANSIENT_TTL      = 600;

	/**
	 * Load pattern arrays from encrypted .dat files in uploads.
	 *
	 * @param bool $base64 If true, load base64_patterns; otherwise load definitions.
	 * @return array Keys: patterns_raw, patterns_iraw, patterns_re (or patterns_b64functions, patterns_b64keywords).
	 */
	public static function load_patterns_from_uploads( $base64 = false ) {
		$upload_dir  = wp_upload_dir();
		$base_dir    = $upload_dir['basedir'] . '/security-ninja/';
		$definitions = $base_dir . 'definitions/';
		$base64_dir  = $base_dir . 'base64_patterns/';

		$out = array(
			'patterns_raw'          => array(),
			'patterns_iraw'         => array(),
			'patterns_re'           => array(),
			'patterns_b64functions' => array(),
			'patterns_b64keywords'  => array(),
		);

		if ( ! $base64 ) {
			$out['patterns_raw']  = self::load_one_pattern_file( $definitions . 'patterns_raw.dat' );
			$out['patterns_iraw'] = self::load_one_pattern_file( $definitions . 'patterns_iraw.dat' );
			$out['patterns_re']   = self::load_one_pattern_file( $definitions . 'patterns_re.dat' );
		} else {
			$out['patterns_b64functions'] = self::load_one_pattern_file( $base64_dir . 'php_functions.dat' );
			$out['patterns_b64keywords']  = self::load_one_pattern_file( $base64_dir . 'php_keywords.dat' );
		}

		return $out;
	}

	/**
	 * Load and parse one encrypted .dat file (decrypt, then # = comment, else pattern => comment).
	 *
	 * @param string $file Full path to .dat file.
	 * @return array Pattern => comment.
	 */
	public static function load_one_pattern_file( $file ) {
		$list         = array();
		$last_comment = '';
		if ( ! is_readable( $file ) ) {
			return $list;
		}
		$contents    = file_get_contents( $file );
		$decrypted   = Wf_Sn_Ms::string_crypt( $contents, 'd' );
		$pattern_arr = explode( "\n", $decrypted );
		foreach ( $pattern_arr as $pattern ) {
			if ( strlen( trim( $pattern ) ) === 0 ) {
				continue;
			}
			if ( $pattern[0] === '#' ) {
				$last_comment = $pattern;
				continue;
			}
			$list[ trim( $pattern ) ] = trim( $last_comment );
		}
		return $list;
	}

	/**
	 * Filename for file-based progress (avoids set_transient during scan = no Query Monitor backtrace = no memory spike).
	 */
	const PROGRESS_FILENAME = 'scan_progress.json';

	/**
	 * Minimum seconds between progress updates.
	 */
	const PROGRESS_THROTTLE_SEC = 10;

	/**
	 * Get the full path to the progress file (in uploads/security-ninja/).
	 *
	 * @return string Path to file, or empty if uploads not writable.
	 */
	private static function get_progress_file_path() {
		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			return '';
		}
		$dir = $upload_dir['basedir'] . '/security-ninja';
		if ( ! wp_mkdir_p( $dir ) ) {
			return '';
		}
		return $dir . '/' . self::PROGRESS_FILENAME;
	}

	/**
	 * Set current scan path for progress UI using a file (not transient) to avoid
	 * triggering set_transient hooks during deep recursion, which can exhaust memory
	 * when plugins like Query Monitor build a full backtrace. Throttled to PROGRESS_THROTTLE_SEC.
	 *
	 * @param string $path  Current directory or path being scanned.
	 * @param array  $stats Optional. Current counts: files_scanned, directories_scanned.
	 */
	public static function set_scan_progress( $path, $stats = array() ) {
		$file = self::get_progress_file_path();
		if ( $file === '' ) {
			return;
		}
		if ( is_readable( $file ) ) {
			$raw = @file_get_contents( $file );
			if ( $raw !== false ) {
				$existing = json_decode( $raw, true );
				if ( is_array( $existing ) && isset( $existing['updated'] ) ) {
					if ( ( time() - (int) $existing['updated'] ) < self::PROGRESS_THROTTLE_SEC ) {
						return;
					}
				}
			}
		}
		$data = array(
			'current_path'        => $path,
			'updated'             => time(),
			'files_scanned'       => isset( $stats['files_scanned'] ) ? (int) $stats['files_scanned'] : 0,
			'directories_scanned' => isset( $stats['directories_scanned'] ) ? (int) $stats['directories_scanned'] : 0,
		);
		@file_put_contents( $file, wp_json_encode( $data ), LOCK_EX );
	}

	/**
	 * Get current scan progress from file (no transient, no hooks).
	 *
	 * @return array With current_path, current_path_display, updated, files_scanned, directories_scanned; or empty array.
	 */
	public static function get_scan_progress() {
		$file = self::get_progress_file_path();
		if ( $file === '' || ! is_readable( $file ) ) {
			return array();
		}
		$raw = @file_get_contents( $file );
		if ( $raw === false ) {
			return array();
		}
		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			return array();
		}
		if ( ! empty( $data['current_path'] ) ) {
			$data['current_path_display'] = self::path_for_display( $data['current_path'] );
		}
		if ( ! isset( $data['files_scanned'] ) ) {
			$data['files_scanned'] = 0;
		}
		if ( ! isset( $data['directories_scanned'] ) ) {
			$data['directories_scanned'] = 0;
		}
		return $data;
	}

	/**
	 * Clear scan progress file and any legacy transient (call when scan finishes).
	 */
	public static function clear_scan_progress() {
		$file = self::get_progress_file_path();
		if ( $file !== '' && is_file( $file ) ) {
			@unlink( $file );
		}
		delete_transient( self::TRANSIENT_PROGRESS );
	}

	/**
	 * Get path relative to ABSPATH for display (avoids long absolute paths in UI).
	 *
	 * @param string $path Full path.
	 * @return string Relative path, or original if ABSPATH not defined or not a prefix.
	 */
	public static function path_for_display( $path ) {
		if ( ! defined( 'ABSPATH' ) || $path === '' ) {
			return $path;
		}
		$base = rtrim( str_replace( '\\', '/', ABSPATH ), '/' );
		$path = str_replace( '\\', '/', $path );
		if ( $base !== '' && strpos( $path, $base ) === 0 ) {
			$path = substr( $path, strlen( $base ) );
			return ltrim( $path, '/' );
		}
		return $path;
	}

	/**
	 * Get advanced whitelist rules (pattern + path and path-only).
	 *
	 * @return array List of rules with path_pattern, and optionally pattern and description.
	 */
	public static function get_advanced_whitelist_rules() {
		$pattern_based_rules = array(
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backup-pro/Base.php',
				'description'  => 'WPVivid Backup Pro - phpseclib cryptography library (legitimate system info function)',
			),
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backup-pro/Hash.php',
				'description'  => 'WPVivid Backup Pro - phpseclib cryptography library (legitimate system info function)',
			),
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backup-pro/ClientResolver.php',
				'description'  => 'WPVivid Backup Pro - AWS library (legitimate OS detection for user agent)',
			),
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backup-pro/class-wpvivid-pclzip.php',
				'description'  => 'WPVivid Backup Pro - WordPress Pclzip class (legitimate OS detection for path handling)',
			),
			array(
				'pattern'      => '\$[a-z]+\(\$[a-z0-9]+\(',
				'path_pattern' => 'wpvivid-backup-pro/Middleware.php',
				'description'  => 'WPVivid Backup Pro - AWS library (legitimate middleware pattern with instanceof check)',
			),
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backuprestore/includes/zip/class-wpvivid-pclzip.php',
				'description'  => 'WPVivid Free - WordPress Pclzip class (legitimate OS detection for path handling)',
			),
			array(
				'pattern'      => 'php_uname\(["\'asrvm]+\)',
				'path_pattern' => 'wpvivid-backuprestore/includes/class-wpvivid.php',
				'description'  => 'WPVivid Free - Debug page (legitimate OS detection for user debugging)',
			),
		);

		$path_only_rules = array(
			array(
				'path_pattern' => '*/wp-includes/js/dist/*',
				'description'  => 'WordPress core - Gutenberg and block editor JS bundles (often match generic malware heuristics)',
			),
			array(
				'path_pattern' => '*/wp-admin/js/*',
				'description'  => 'WordPress core - admin dashboard JS bundles',
			),
			array(
				'path_pattern' => '*/plugins/redirection/models/regex.php',
				'description'  => 'Redirection - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/bb-ultimate-addon/includes/column-js.php',
				'description'  => 'BB Ultimate Addon - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/bb-ultimate-addon/includes/row-js.php',
				'description'  => 'BB Ultimate Addon - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/admin-site-enhancements-pro/includes/premium/admin-columns/sortable-item-bar.php',
				'description'  => 'Admin Site Enhancements Pro - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/bb-plugin/modules/pricing-table/includes/frontend.css.php',
				'description'  => 'Beaver Builder - known safe file',
			),
			array(
				'path_pattern' => '*.DS_Store',
				'description'  => 'macOS system file',
			),
			array(
				'path_pattern' => '*/js_composer/vc_classmap.json.php',
				'description'  => 'Visual Composer - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/pretty-link/pro/*',
				'description'  => 'Pretty Link Pro - known safe plugin',
			),
			array(
				'path_pattern' => '*/Divi/includes/builder/frontend-builder/helpers.php',
				'description'  => 'Divi theme - known safe file',
			),
			array(
				'path_pattern' => '*/wp-snapshots/*',
				'description'  => 'WP Snapshots - backup system',
			),
			array(
				'path_pattern' => '*/plugins/jetpack/*',
				'description'  => 'Jetpack - known safe plugin',
			),
			array(
				'path_pattern' => '*/uploads/backupbuddy_temp/*',
				'description'  => 'BackupBuddy temporary files',
			),
			array(
				'path_pattern' => '*/uploads/pb_backupbuddy/*',
				'description'  => 'BackupBuddy files',
			),
			array(
				'path_pattern' => '*/plugins/security-ninja/*',
				'description'  => 'Security Ninja plugin files',
			),
			array(
				'path_pattern' => '*/plugins/security-ninja-premium/*',
				'description'  => 'Security Ninja Premium plugin files',
			),
			array(
				'path_pattern' => '*/plugins/seo-booster-premium/*',
				'description'  => 'SEO Booster Premium - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/backupbuddy/*',
				'description'  => 'BackupBuddy - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/wp-spamshield/*',
				'description'  => 'WP SpamShield - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/login-ninja/*',
				'description'  => 'Login Ninja - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/gravityforms/gravityforms.php',
				'description'  => 'Gravity Forms - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/5sec-google-authenticator/*',
				'description'  => 'Google Authenticator - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/google-maps-widget/*',
				'description'  => 'Google Maps Widget - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/optin-ninja/*',
				'description'  => 'Optin Ninja - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/smartmonitor/*',
				'description'  => 'Smart Monitor - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/under-construction-page/*',
				'description'  => 'Under Construction Page - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/profit_builder/*',
				'description'  => 'Profit Builder - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/leadsflow-pro/*',
				'description'  => 'LeadsFlow Pro - known safe plugin',
			),
			array(
				'path_pattern' => '*/wflogs/*',
				'description'  => 'Wordfence logs',
			),
			array(
				'path_pattern' => '*/themes/enfold/includes/admin/demo_files/*',
				'description'  => 'Enfold theme demo files',
			),
			array(
				'path_pattern' => '*/freemius/templates/secure-https-header.php',
				'description'  => 'Freemius - known safe file',
			),
			array(
				'path_pattern' => '*/themes/enfold/css/dynamic-css.php',
				'description'  => 'Enfold theme dynamic CSS',
			),
			array(
				'path_pattern' => '*/tcpdf/tcpdf_barcodes_1d.php',
				'description'  => 'TCPDF barcode library',
			),
			array(
				'path_pattern' => '*/plugins/wp-seo-keyword-optimizer-premium/classes/class.import.php',
				'description'  => 'SEO Keyword Optimizer - known safe file',
			),
			array(
				'path_pattern' => '*/phpseclib/Crypt/*',
				'description'  => 'phpseclib cryptography library',
			),
			array(
				'path_pattern' => '*/guzzle/src/*',
				'description'  => 'Guzzle HTTP library',
			),
			array(
				'path_pattern' => '*/dompdf/lib/fonts/*',
				'description'  => 'DOMPDF font library',
			),
			array(
				'path_pattern' => '*/plugins/wp-seo-keyword-optimizer-premium/admin/controller/controller.php',
				'description'  => 'SEO Keyword Optimizer - known safe file',
			),
			array(
				'path_pattern' => '*/plugins/wp-reset/*',
				'description'  => 'WP Reset - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/minimal-coming-soon-maintenance-mode/*',
				'description'  => 'Minimal Coming Soon - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/wp-seopress-pro/*',
				'description'  => 'SEOPress Pro - known safe plugin',
			),
			array(
				'path_pattern' => '*/plugins/clsop/inc/Engine/Optimization/DeferJS/DeferJS.php',
				'description'  => 'CLSOP DeferJS - known safe file',
			),
		);

		$all_rules = array_merge( $pattern_based_rules, $path_only_rules );
		return apply_filters( 'securityninja_advanced_whitelist_rules', $all_rules );
	}

	/**
	 * Check if path is whitelisted (advanced path-only rules, user whitelist by filename, legacy get_whitelist fnmatch).
	 *
	 * @param string $path File path.
	 * @return bool
	 */
	public static function is_path_whitelisted( $path ) {
		$advanced_rules = self::get_advanced_whitelist_rules();
		foreach ( $advanced_rules as $rule ) {
			if ( ! isset( $rule['pattern'] ) ) {
				if ( fnmatch( $rule['path_pattern'], strtolower( $path ) ) ) {
					return true;
				}
			}
		}

		$whitelist = get_option( 'wf_sn_ms_whitelist', array() );
		if ( is_array( $whitelist ) ) {
			foreach ( $whitelist as $item ) {
				if ( isset( $item['filename'] ) && $item['filename'] === $path ) {
					return true;
				}
			}
		}

		$legacy_whitelist = Wf_Sn_Ms::get_whitelist();
		foreach ( $legacy_whitelist as $pattern ) {
			if ( fnmatch( $pattern, strtolower( $path ) ) ) {
				return true;
			}
		}

		// User path/pattern entries (per-file paths + pattern textarea).
		foreach ( Wf_Sn_Ms::get_whitelist_paths_and_patterns() as $pattern ) {
			if ( fnmatch( $pattern, strtolower( $path ) ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if a detected pattern should be whitelisted by advanced rules (path + pattern or path-only).
	 *
	 * @param string $path              File path.
	 * @param string $detected_pattern  Pattern that was matched.
	 * @return bool
	 */
	public static function matches_advanced_whitelist_pattern( $path, $detected_pattern ) {
		$advanced_rules = self::get_advanced_whitelist_rules();
		$path_lower     = strtolower( $path );
		foreach ( $advanced_rules as $rule ) {
			$path_match = ( strpos( $rule['path_pattern'], '*' ) !== false )
				? fnmatch( $rule['path_pattern'], $path_lower )
				: ( strpos( $path_lower, $rule['path_pattern'] ) !== false );
			if ( ! $path_match ) {
				continue;
			}
			if ( ! isset( $rule['pattern'] ) ) {
				return true;
			}
			if ( strpos( $detected_pattern, $rule['pattern'] ) !== false ) {
				return true;
			}
		}
		return false;
	}
}
