<?php
namespace WPSecurityNinja\Plugin;

/*
 * Copyright (c) 2016 Gabor Gyorvari
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
Extended functionality by Security Ninja - removed CLI functionality and return results in json.

 * @deprecated The Malware tab now uses MalwareScannerAdapter (scr34m/php-malware-scanner + Util).
 *             This file is kept for reference only; the scan is triggered via modules/malware-scanner/class-malware-scanner-adapter.php
 */
class MalwareScanner {

	private $dir               = '';
	private $extension         = array( '.php', '.ico', '.inc' ); // @todo
	private $flagBase64        = false;
	private $flagChecksum      = false;
	private $flagComments      = false;
	private $flagHideOk        = false;
	private $flagHideWhitelist = true;
	private $flagNoStop        = false;
	private $flagPattern       = false;

	private $flagExtraCheck        = false;
	private $flagFollowSymlink     = false;
	private $flagLineNumber        = false;
	private $flagScanEverything    = false;
	private $flagCombinedWhitelist = false;
	private $whitelist             = array();
	private $ignore                = array();

	private $stat = array(
		'directories'    => 0,
		'files_scanned'  => 0,
		'files_infected' => 0,
		'results'        => array(),
	);



		//Pattern File Attributes
	private $patterns_raw             = array();
	private $patterns_iraw            = array();
	private $patterns_re              = array();
	private $patterns_b64functions    = array();
	private $patterns_b64keywords     = array();
	private $combined_whitelist       = array();
	private $combined_whitelist_count = 0;

		/**
		 * MalwareScanner constructor.
		 *
		 * @param bool $cli defines its calling from commandline or using as a library, default is true
		 */
	public function __construct() {
		// No cli - removed
	}



		//Handles pattern loading and saving to the class object
	private function initializePatterns() {
		//  $dir = dirname(__FILE__);
			//Loads either the primary scanning patterns or the base64 patterns depending on -b/--base64 flag
		$upload_dir            = wp_upload_dir();
		$secninjaUploadDir     = $upload_dir['basedir'] . '/security-ninja/';
		$patternsfoldername    = $secninjaUploadDir . 'base64_patterns/';
		$definitionsfoldername = $secninjaUploadDir . 'definitions/';

		if ( ! $this->flagBase64 ) {
			$this->patterns_raw  = $this->loadPatterns( $definitionsfoldername . 'patterns_raw.dat' );
			$this->patterns_iraw = $this->loadPatterns( $definitionsfoldername . 'patterns_iraw.dat' );
			$this->patterns_re   = $this->loadPatterns( $definitionsfoldername . 'patterns_re.dat' );
		} else {
			$this->patterns_b64functions = $this->loadPatterns( $patternsfoldername . 'php_functions.dat' );
			$this->patterns_b64keywords  = $this->loadPatterns( $patternsfoldername . 'php_keywords.dat' );
		}

			//Adds additional checks to patterns_raw
			//This may be something to move into a pattern file rather than leave hardcoded.
		if ( $this->flagExtraCheck ) {
			$this->patterns_raw['googleBot'] = '# ';
			$this->patterns_raw['htaccess']  = '# ';
		}
	}

		//Check if the md5 checksum exists in the whitelist and returns true if it does.
	private function inWhitelist( $hash ) {
		if ( $this->flagCombinedWhitelist ) {
			if ( $this->binarySearch( $hash, $this->combined_whitelist, $this->combined_whitelist_count ) > -1 ) {
				return true;
			}
		}
		return in_array( $hash, $this->whitelist );
	}

		//Check if -i/--ignore flag listed this path to be omitted.
	private function isIgnored( $pathname ) {
		// @todo - match in_array - faster but also wildcard?
		foreach ( $this->ignore as $pattern ) {
			$match = $this->pathMatches( $pathname, $pattern );
			if ( $match ) {
				return true;
			}
		}
		return false;
	}


		// @todo
		//Loads individual pattern files
		//Skips blank linese
	// Loads encrypted content and converts to plaintext
		//Stores most recent comment with the pattern in the list[] array
		//Returns an array of patterns:comments in key:value pairs
	private function loadPatterns( $file ) {
		$last_comment = '';
		$list         = array();
		if ( is_readable( $file ) ) {
			$contents    = file_get_contents( $file );
			$decrypted   = wf_sn_ms::string_crypt( $contents, 'd' );
			$pattern_arr = explode( "\n", $decrypted );

			foreach ( $pattern_arr as $pattern ) {
							//Check if the line is only whitespace and skips.
				if ( strlen( trim( $pattern ) ) == 0 ) {
					continue;
				}
				//Check if first char in pattern is a '#' which indicates a comment and skips.
				//Stores the comment to be stored with the pattern in the list as key:value pairs.
				//The pattern is the key and the comment is the value.
				if ( $pattern[0] === '#' ) {
					$last_comment = $pattern;
					continue;
				}
				$list[ trim( $pattern ) ] = trim( $last_comment );
			}
		}
		return $list;
	}



	// @todo - fix desc - modified to add ignore to the wp-admin and wp-includes folders - covered in core scanner
	private function addWordpressChecksums( $wp_version, $wp_locale ) {
		$ver    = get_bloginfo( 'version' );
		$locale = get_locale();

		$ignorelist = array(
			ABSPATH . 'wp-admin/',
			trailingslashit( ABSPATH . WPINC ),
		);

		$this->whitelist = $ignorelist;
	}


	public function setExtensions( array $a ) {
		$this->extension = array();
		foreach ( $a as $ext ) {
			if ( $ext[0] != '.' ) {
				$ext = '.' . $ext;
			}
			$this->extension[] = strtolower( $ext );
		}
	}

	public function setIgnore( array $a ) {
		$this->ignore = $a;
	}

	public function setFlagChecksum( $b ) {
		$this->flagChecksum = $b;
	}

	public function setFlagComments( $b ) {
		$this->flagComments = $b;
	}

	public function setFlagPattern( $b ) {
		$this->flagPattern = $b;
	}

	public function setFlagLineNumber( $b ) {
		$this->flagLineNumber = $b;
	}

	public function setFlagBase64( $b ) {
		$this->flagBase64 = $b;
	}

	public function setFlagExtraCheck( $b ) {
		$this->flagExtraCheck = $b;
	}

	public function setFlagFollowSymlink( $b ) {
		$this->flagFollowSymlink = $b;
	}

	public function setFlagHideOk( $b ) {
		$this->flagHideOk = $b;
	}

	public function setFlagHideWhitelist( $b ) {
		$this->flagHideWhitelist = $b;
	}

	public function setFlagNoStop( $b ) {
		$this->flagNoStop = $b;
	}

	public function setFlagScanEverything( $b ) {
		$this->flagScanEverything = $b;
	}

	public function setFlagCombinedWhitelist( $b ) {
		$this->flagCombinedWhitelist = $b;
	}

		// @see http://stackoverflow.com/a/13914119
	private function pathMatches( $path, $pattern, $ignoreCase = false ) {
		$expr = preg_replace_callback(
			'/[\\\\^$.[\\]|()?*+{}\\-\\/]/',
			function ( $matches ) {
				switch ( $matches[0] ) {
					case '*':
						return '.*';
					case '?':
						return '.';
					default:
						return '\\' . $matches[0];
				}
			},
			$pattern
		);

			// Matching array with filename and hash
		if ( is_array( $expr ) ) {
			$expr = '/' . $expr['filename'] . '/';

			if ( $ignoreCase ) {
				$expr .= 'i';
			}
		} else {
			$expr = '/' . $expr . '/';
			if ( $ignoreCase ) {
				$expr .= 'i';
			}
		}

		return (bool) preg_match( $expr, $path );
	}

		/**
		 * Formats and prints the scan result output line by line.
		 *
		 * Depending on specified options, it will print:
		 * - Status code
		 * - Last Modified Time
		 * - MD5 Hash
		 * - File Path
		 * - Pattern Matched
		 * - The last comment to appear in the pattern file before this pattern
		 * - Matching line number
		 *
		 * @param $found
		 * @param $path
		 * @param $pattern
		 * @param $comment
		 * @param $hash
		 * @param $lineNumber
		 * @param bool $inWhitelist
		 */
	private function printPath( $found, $path, $pattern, $comment, $hash, $lineNumber, $matchedLine = '' ) {
		if ( ! $pattern ) {
			return;
		}

		// Check if this detected pattern matches advanced whitelist rules
		if ( $this->matches_advanced_whitelist_pattern( $path, $pattern ) ) {
			return; // Skip this pattern - don't add it to results
		}

		$state        = 'ER'; // default state
		$changed_time = filectime( $path );
		$ctime        = gmdate( 'H:i d-m-Y', $changed_time );

		$result = array(
			'state'       => $state,
			'ctime'       => $ctime,
			'hash'        => $hash,
			'path'        => $path,
			'pattern'     => $pattern,
			'comment'     => $comment,
			'line'        => $lineNumber,
			'matchedline' => $matchedLine,
		);

		$this->stat['files'][] = $result;

		//echo str_replace(array_keys($map), array_values($map), $format) . PHP_EOL;
	}

	/**
	 * Recursively scales the file system.
	 * Calls the scan() function for each file found.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Friday, September 27th, 2024.
	 * @access  private
	 * @param   mixed   $dir
	 * @return  void
	 */
	private function process( $dir ) {

		$dh = opendir( $dir );
		if ( ! $dh ) {
			return;
		}

		if ( $this->inWhitelist( $dir ) ) {
			return;
		}

		++$this->stat['directories'];
		$this->stat['last_processed_dir'] = $dir;
		while ( ( $file = readdir( $dh ) ) !== false ) {
			if ( $file == '.' || $file == '..' ) {
				continue;
			}
			if ( $this->isIgnored( $dir . $file ) ) {
				continue;
			}
			if ( ! $this->flagFollowSymlink && is_link( $dir . $file ) ) {
				continue;
			}
			if ( is_dir( $dir . $file ) ) {
				$this->process( $dir . $file . '/' );
			} elseif ( is_file( $dir . $file ) ) {
				$ext = strtolower( substr( $file, strrpos( $file, '.' ) ) );
				if ( $this->flagScanEverything || in_array( $ext, $this->extension ) ) {
					$this->scan( $dir . $file );
				}
			}
		}
		//        $WF_SN_MS_RESULTS = get_option(WF_SN_MS_RESULTS); do_mal_scan

		// @lars - updates the database with stats, can slow down process.
		// A simple tests shows a scan go from 55 sec to 47 sec
		/*
		$WF_SN_MS_RESULTS = get_option(WF_SN_MS_RESULTS);
		$WF_SN_MS_RESULTS['do_mal_scan'] = $this->stat;
		update_option( WF_SN_MS_RESULTS , $WF_SN_MS_RESULTS);
		*/

		closedir( $dh );
	}




	// @lars @new
	public function do_scan( $dir ) {
			// Make sure the input is a valid directory path.
		$dir = rtrim( $dir, '/' );
		if ( ! is_dir( $dir ) ) {
			//$this->error('Specified path is not a directory: ' . $dir);
			return false;
		}
		$this->stat['status'] = 'running';

		$this->initializePatterns();
		global $wp_version;
		$this->addWordpressChecksums( $wp_version, get_locale() );

		if ( $this->flagCombinedWhitelist && ! $this->updateCombinedWhitelist() ) {
			return false;
		}

		$start = time();

		$this->process( $dir . '/' );
		$end                  = time();
		$this->stat['status'] = 'finished';
		if ( ! isset( $this->stat['files'] ) ) {
			$this->stat['files'] = array();
		}
		$results = array(
			'start_time'          => gmdate( 'Y-m-d h:i:s', $start ),
			'end_time'            => gmdate( 'Y-m-d h:i:s', $end ),
			'total_time'          => ( $end - $start ),
			'base_directory'      => $dir,
			'directories_scanned' => $this->stat['directories'],
			'files_scanned'       => $this->stat['files_scanned'],
			'malware_identified'  => $this->stat['files_infected'],
			'files'               => $this->stat['files'],
			'scan_status'         => $this->stat['status'],
			'patterns_used'       => count( $this->patterns_raw ) + count( $this->patterns_iraw ) + count( $this->patterns_re ),
			'whitelist_entries'   => count( $this->whitelist ),
			'memory_usage'        => memory_get_peak_usage( true ),
			'php_version'         => PHP_VERSION,
		);
		return $results;
	}

	/**
	 * Get advanced whitelist rules for pattern + path matching
	 *
	 * @return array Array of advanced whitelist rules
	 */
	public function get_advanced_whitelist_rules() {
		// Advanced whitelist rules that match both pattern AND file path
		// This prevents false positives for legitimate files that contain suspicious patterns
		$pattern_based_rules = array(
			// WPVivid Backup Pro - phpseclib and AWS library files
			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)',
			),
			// WPVivid Free Version - WordPress Pclzip class
			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 whitelist rules (no pattern matching required)
		$path_only_rules = array(
			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',
			),
			// System files
			array(
				'path_pattern' => '*.DS_Store',
				'description'  => 'macOS system file',
			),
			// Known safe plugins and themes
			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',
			),
		);

		// Combine both types of rules
		$all_rules = array_merge( $pattern_based_rules, $path_only_rules );

		// Allow filtering via WordPress hook
		return apply_filters( 'securityninja_advanced_whitelist_rules', $all_rules );
	}

	/**
	 * Check if a file should be whitelisted by path only (before scanning)
	 *
	 * @param string $path The file path
	 * @return bool Whether the file should be whitelisted based on path
	 */
	private function is_path_whitelisted( $path ) {
		// Check advanced whitelist rules first
		$advanced_rules = $this->get_advanced_whitelist_rules();

		foreach ( $advanced_rules as $rule ) {
			// Only check path-only rules (no pattern field)
			if ( ! isset( $rule['pattern'] ) ) {
				// Use fnmatch for folder patterns (supports wildcards like */)
				if ( fnmatch( $rule['path_pattern'], strtolower( $path ) ) ) {
					return true; // Path-only whitelist rule matched
				}
			}
		}

		// Check database-based whitelist (user-added files)
		$whitelist = get_option( 'wf_sn_ms_whitelist', array() );
		foreach ( $whitelist as $whitelist_item ) {
			if ( $whitelist_item['filename'] === $path ) {
				return true; // User-whitelisted file
			}
		}

		// Check legacy internal whitelist (for backward compatibility)
		$legacy_whitelist = wf_sn_ms::get_whitelist();
		foreach ( $legacy_whitelist as $pattern ) {
			if ( fnmatch( $pattern, strtolower( $path ) ) ) {
				return true; // Legacy whitelist pattern matched
			}
		}

		return false;
	}

	/**
	 * Check if a detected pattern matches advanced whitelist rules (pattern + path)
	 *
	 * @param string $path The file path
	 * @param string $detectedPattern The specific pattern that was detected
	 * @return bool Whether the pattern should be whitelisted based on advanced rules
	 */
	private function matches_advanced_whitelist_pattern( $path, $detectedPattern ) {
		$advanced_rules = $this->get_advanced_whitelist_rules();

		foreach ( $advanced_rules as $rule ) {
			// Check if file path matches the rule's path pattern
			$pathMatch = strpos( $path, $rule['path_pattern'] ) !== false;

			if ( $pathMatch ) {
				// For path-only rules (no pattern field), whitelist immediately
				if ( ! isset( $rule['pattern'] ) ) {
					return true; // Path-only whitelist rule matched
				}

				// For pattern-based rules, check if the detected pattern matches
				$patternMatch = strpos( $detectedPattern, $rule['pattern'] ) !== false;

				if ( $patternMatch ) {
					return true; // Pattern matches advanced whitelist rule
				}
			}
		}
		return false;
	}

	/**
	 * Check if the path is whitelisted
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @param   mixed   $path
	 * @return  boolean
	 */
	function check_in_wf_sn_whitelist( $path ) {
		$internalWhitelist = wf_sn_ms::get_whitelist();
		$testpath          = $path;
		$testpath          = str_replace( ABSPATH, '', $testpath );

		foreach ( $internalWhitelist as $ig ) {
			if ( stristr( $testpath, $ig ) ) {
				return $ig;
			}
		}
		return false;
	}



		//Loads target file contents for scanning
		//Initiates the multiple scan types by calling the scanLoop function
	public function scan( $path ) {
		++$this->stat['files_scanned'];

		// Check advanced path-based whitelist first (most efficient)
		if ( $this->is_path_whitelisted( $path ) ) {
			return false; // File is whitelisted, skip scanning
		}

		$fileContent = file_get_contents( $path );
		$found       = false;
		$inWhitelist = false;
		$hash        = md5( $fileContent );
		$toSearch    = '';
		$comment     = '';

		// Check hash-based whitelist (legacy system)
		if ( $this->inWhitelist( $hash ) ) {
			$inWhitelist = true;
		} elseif ( $this->check_in_wf_sn_whitelist( $path ) ) {
			$inWhitelist = true;
		} elseif ( ! $this->flagBase64 ) {
			$this->scanLoop( 'scanFunc_STR', $fileContent, $this->patterns_raw, $path, $found, $hash );
			$this->scanLoop( 'scanFunc_STRI', $fileContent, $this->patterns_iraw, $path, $found, $hash );
			$this->scanLoop( 'scanFunc_RE', $fileContent, $this->patterns_re, $path, $found, $hash );
		} else {
			$this->scanLoop( 'scanFunc_STR', $fileContent, $this->patterns_b64functions, $path, $found, $hash );
			$this->scanLoop( 'scanFunc_STR', $fileContent, $this->patterns_b64keywords, $path, $found, $hash );
		}

		if ( ! $found ) {
			$this->printPath( $found, $path, $toSearch, $comment, $hash, 0 );
			return false;
		}

		++$this->stat['files_infected'];
		return true;
	}

		//Performs raw string, case sensitive matching.
		//Returns true if the raw string exists in the file contents.
	private function scanFunc_STR( &$pattern, &$content ) {
		return strpos( $content, $pattern );
	}

		//Performs raw string, case insensitive matching.
		//Returns true if the raw string exists in the file contents, ignoring case.
	private function scanFunc_STRI( &$pattern, &$content ) {
		return stripos( $content, $pattern );
	}

		//Performs regular expression matching.
		//Returns true if the Regular Expression matches something in the file.
		//Patterns will match multiple lines, though you can use ^$ to match the beginning and end of a line.
	private function scanFunc_RE( &$pattern, &$content ) {
		$ret = preg_match( '/' . $pattern . '/im', $content, $match, PREG_OFFSET_CAPTURE );
		if ( $ret ) {
			return $match[0][1];
		}
		return false;
	}

		//First parameter '$scanFunction' is a defined function name passed as a string.
		//This function should accept a pattern string and a content string.
		//This function will return true if the pattern exists in the content.
		//See 'scanFunc_STR', 'scanFunc_STRI', 'scanFUNC_RE' above as examples.

		//Loops through all patterns in a file using the passed function name to determine a match.
		//Variables passed by reference for performance and modification access.
	private function scanLoop( $scanFunction, &$fileContent, &$patterns, &$path, &$found, $hash ) {
		if ( ! $found || $this->flagNoStop ) {
			foreach ( $patterns as $pattern => $comment ) {
								//Call the function that is named in $scanFunction
								//This allows multiple search/match functions to be used without duplicating the loop code.
				$position = $this->$scanFunction( $pattern, $fileContent );
				if ( $position !== false ) {
					$found      = true;
					$lineNumber = 0;
					if ( $this->flagLineNumber ) {
						if ( $pos = strrpos( substr( $fileContent, 0, $position ), "\n" ) ) {
							$lineNumber = substr_count( substr( $fileContent, 0, $pos + 1 ), "\n" ) + 1;
						}
					}

						// grab relevant line
					$fileArray   = explode( "\n", $fileContent );
					$matchedLine = '';
					if ( $lineNumber ) {
						$matchedLine = esc_html( substr( $fileArray[ $lineNumber - 1 ], 0, 210 ) );
					}

					unset( $fileArray );
					$this->printPath( $found, $path, $pattern, $comment, $hash, $lineNumber, $matchedLine );
					if ( ! $this->flagNoStop ) {
						return;
					}
				}
			}
		}
	}

		// @see https://www.mkwd.net/binary-search-algorithm-in-php/
	private function binarySearch( $needle, array $haystack, $high, $low = 0 ) {
		$key = false;
				// Whilst we have a range. If not, then that match was not found.
		while ( $high >= $low ) {
						// Find the middle of the range.
			$mid = (int) floor( ( $high + $low ) / 2 );
						// Compare the middle of the range with the needle. This should return <0 if it's in the first part of the range,
						// or >0 if it's in the second part of the range. It will return 0 if there is a match.
			$cmp = strcmp( $needle, $haystack[ $mid ] );
						// Adjust the range based on the above logic, so the next loop iteration will use the narrowed range
			if ( $cmp < 0 ) {
				$high = $mid - 1;
			} elseif ( $cmp > 0 ) {
				$low = $mid + 1;
			} else {
				$key = $mid;
				break;
			}
		}

		return $key;
	}

	private function updateCombinedWhitelist( $url = 'https://scr34m.github.io/php-malware-scanner' ) {
		$latest_hash = trim( file_get_contents( $url . '/database/compressed.sha256' ) );
		if ( $latest_hash === false ) {
			//  $this->error('Unable to download database checksum');
			return false;
		}

		$file = __DIR__ . '/whitelist.dat';
		if ( is_readable( $file ) ) {
			$hash = hash_file( 'sha256', $file );
			if ( $hash != $latest_hash ) {
				$download = true;
			} else {
				$download = false;
			}
		} else {
			$download = true;
		}

		if ( $download ) {
			$data = file_get_contents( $url . '/database/compressed.dat' );
			if ( $data === false ) {
					//$this->error('Unable to download database');
				return false;
			}

			file_put_contents( $file, $data );
			$hash = hash_file( 'sha256', $file );
			if ( $hash != $latest_hash ) {
					//$this->error('Downloaded database hash mismatch');
			}
		}

		$content                        = gzdecode( file_get_contents( $file ) );
		$this->combined_whitelist       = array();
		$this->combined_whitelist_count = 0;
		foreach ( explode( "\n", $content ) as $line ) { // faster than strtok, but needs more memory
			if ( $line ) {
				$this->combined_whitelist[] = $line;
				++$this->combined_whitelist_count;
			}
		}
				$this->combined_whitelist_count -= 1; // -1 because we use indexes in binary search
				echo 'Combined whitelist records count: ' . ( $this->combined_whitelist_count + 1 ) . PHP_EOL;
				return true;
	}
}

// script it's self called and not included
if ( isset( $argv[0] ) && realpath( $argv[0] ) == realpath( __FILE__ ) ) {
	new MalwareScanner();
}
