<?php
/**
 * Scan Engine
 * Version:     3.0.4
 */

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

final class LVSW_Scanner {

	const REMOTE_SIGNATURES_URL = 'https://store.lvdynamic.com/lvsw-a9f3k2m7xq/lvsw-signatures.json';

	public static function initialize_baselines() {
		update_option( LVSW_Plugin::PLUGINS_OPTION, self::list_plugin_directories() );
		update_option( LVSW_Plugin::ADMINS_OPTION, self::list_admin_users_full() );
		update_option( LVSW_Plugin::HASH_OPTION, self::hash_all_js_files() );
		update_option( LVSW_Plugin::LOG_OPTION, array() );
		update_option( LVSW_Plugin::PRE_USER_OPTION, array() );
		update_option( LVSW_Plugin::STATS_OPTION, array() );

		LVSW_Helpers::log_event( 'Baselines initialized' );
		LVSW_Cron::reschedule_cron( true );
	}

	private static function is_own_plugin_file( $file ) {
		$file = wp_normalize_path( (string) $file );

		$own_paths = array();

		if ( defined( 'LVSW_PATH' ) ) {
			$own_paths[] = wp_normalize_path( LVSW_PATH );
		}

		if ( defined( 'LVSW_FILE' ) ) {
			$own_paths[] = wp_normalize_path( plugin_dir_path( LVSW_FILE ) );
		}

		$own_paths[] = wp_normalize_path( trailingslashit( WP_PLUGIN_DIR ) . 'lumiverse-security-watchdog-lite/' );

		foreach ( $own_paths as $own_path ) {
			$own_path = trailingslashit( $own_path );

			if ( 0 === strpos( $file, $own_path ) ) {
				return true;
			}
		}

		return false;
	}

	private static function is_suspicious_plugin_folder_name( $plugin_dir ) {
		$plugin_dir = trim( (string) $plugin_dir );

		if ( '' === $plugin_dir ) {
			return false;
		}

		$known_safe = array(
			'woocommerce',
			'elementor',
			'elementor-pro',
			'wordfence',
			'wp-rocket',
			'contact-form-7',
			'classic-editor',
			'advanced-custom-fields',
			'advanced-custom-fields-pro',
			'wordpress-seo',
			'seo-by-rank-math',
			'updraftplus',
			'akismet',
			'jetpack',
			'litespeed-cache',
			'wpforms-lite',
			'wp-mail-smtp',
			'redirection',
			'query-monitor',
			'lumiverse-security-watchdog-lite',
		);

		if ( in_array( strtolower( $plugin_dir ), $known_safe, true ) ) {
			return false;
		}

		$patterns = array(
			'/^[a-f0-9]{10,}-[a-z0-9]{4,}$/i',
			'/^[0-9a-f]{8,}-[a-zA-Z0-9]{5,}$/',
			'/^[a-f0-9]{12,}$/i',
			'/^[a-z0-9]{14,}$/i',
			'/^[0-9]{6,}-[a-zA-Z0-9]{4,}$/',
		);

		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $plugin_dir ) ) {
				return true;
			}
		}

		return false;
	}

	public static function do_scan() {
		$opts = LVSW_Plugin::get_opts();

		if ( empty( $opts['enabled'] ) ) {
			LVSW_Helpers::log_event( 'Scan skipped: plugin disabled in settings' );
			return;
		}

		$now     = time();
		$cutoff  = LVSW_Helpers::fast_mode_cutoff_ts( $opts );
		$details = array();

		$stats = array(
			'ran_at'            => $now,
			'fast_mode_enabled' => ! empty( $opts['fast_mode_enabled'] ) ? 1 : 0,
			'fast_mode_days'    => (int) $opts['fast_mode_days'],
			'files_scanned'     => 0,
			'warnings'          => 0,
			'counts'            => array(),
		);

		$needs_remote_signatures = ! empty( $opts['scan_core_suspicious'] )
			|| ! empty( $opts['scan_js'] )
			|| ! empty( $opts['scan_plugins'] )
			|| ! empty( $opts['scan_themes'] )
			|| ! empty( $opts['scan_muplugins'] )
			|| ! empty( $opts['scan_wpcontent_root'] )
			|| ! empty( $opts['scan_uploads_php'] );

		$remote_signatures = array();

		if ( $needs_remote_signatures ) {
			$remote_signatures = self::get_remote_signatures();

			if ( false === $remote_signatures ) {
				LVSW_Helpers::log_event( 'Malware signature scans skipped because remote signatures could not be loaded' );
				$remote_signatures = false;
			}
		}

		if ( ! empty( $opts['alert_users_can_register'] ) && 1 === (int) get_option( 'users_can_register' ) ) {
			$details['users_can_register'] = array(
				'enabled'     => true,
				'fix_enabled' => ! empty( $opts['fix_disable_user_registration'] ),
			);
			LVSW_Helpers::log_event( 'Alert: users_can_register is enabled' );
		}

		if ( ! empty( $opts['scan_core_recent_mtime'] ) || ( ! empty( $opts['scan_core_suspicious'] ) && false !== $remote_signatures ) ) {
			$core = self::scan_core_integrity_checks(
				! empty( $opts['scan_core_recent_mtime'] ),
				! empty( $opts['scan_core_suspicious'] ) && false !== $remote_signatures,
				false !== $remote_signatures ? $remote_signatures : array(),
				$stats
			);

			if ( ! empty( $core ) ) {
				$details = array_merge( $details, $core );
			}
		}

		if ( ! empty( $opts['scan_js'] ) && false !== $remote_signatures ) {
			$prev_hashes    = get_option( LVSW_Plugin::HASH_OPTION, array() );
			$current_hashes = self::hash_all_js_files();
			$changed        = array();

			foreach ( $current_hashes as $path => $hash ) {
				if ( self::should_skip_js_file( $path ) || self::is_own_plugin_file( $path ) ) {
					continue;
				}

				if ( ! isset( $prev_hashes[ $path ] ) || $prev_hashes[ $path ] !== $hash ) {
					$changed[] = $path;
				}
			}

			$suspected = array();

			foreach ( $changed as $file ) {
				if ( self::is_own_plugin_file( $file ) ) {
					continue;
				}

				$content = @file_get_contents( $file );

				if ( false === $content ) {
					continue;
				}

				$stats['files_scanned']++;

				if ( strlen( $content ) > 600000 ) {
					$content = substr( $content, 0, 600000 );
				}

				$reasons = self::analyse_content_for_signatures( $content, $remote_signatures );

				if ( ! empty( $reasons ) ) {
					$suspected[ $file ] = $reasons;

					if ( count( $suspected ) >= (int) $opts['max_findings_per_group'] ) {
						break;
					}
				}
			}

			update_option( LVSW_Plugin::HASH_OPTION, $current_hashes );

			if ( ! empty( $suspected ) ) {
				$details['js_suspected'] = $suspected;
				LVSW_Helpers::log_event( count( $suspected ) . ' suspicious JS files detected' );
			} else {
				LVSW_Helpers::log_event( count( $changed ) . ' JS files changed, none suspicious' );
			}

			$stats['counts']['js_changed']   = count( $changed );
			$stats['counts']['js_suspected'] = count( $suspected );
		} elseif ( ! empty( $opts['scan_js'] ) ) {
			LVSW_Helpers::log_event( 'JS malware scan skipped: remote signatures unavailable' );
		}

		if ( ! empty( $opts['scan_plugins'] ) ) {
			$prev_plugins = get_option( LVSW_Plugin::PLUGINS_OPTION, array() );
			$now_plugins  = self::list_plugin_directories();
			$new_plugins  = array_values( array_diff( $now_plugins, $prev_plugins ) );
			$deleted      = array_values( array_diff( $prev_plugins, $now_plugins ) );

			if ( ! empty( $new_plugins ) ) {
				$details['new_plugins'] = array_slice( $new_plugins, 0, (int) $opts['max_findings_per_group'] );
			}

			if ( ! empty( $deleted ) ) {
				$details['deleted_plugins'] = array_slice( $deleted, 0, (int) $opts['max_findings_per_group'] );
			}

			update_option( LVSW_Plugin::PLUGINS_OPTION, $now_plugins );
			LVSW_Helpers::log_event( count( $new_plugins ) . ' new plugins, ' . count( $deleted ) . ' deleted plugins' );

			$stats['counts']['new_plugins']     = count( $new_plugins );
			$stats['counts']['deleted_plugins'] = count( $deleted );

			$plugin_results = array();

			if ( false !== $remote_signatures ) {
				$plugin_results = self::scan_php_tree_for_indicators(
					WP_PLUGIN_DIR,
					'plugins',
					$cutoff,
					(int) $opts['max_findings_per_group'],
					$remote_signatures,
					$stats
				);
			} else {
				LVSW_Helpers::log_event( 'Plugin malware signature scan skipped: remote signatures unavailable' );
			}

			$suspicious_plugin_names = array(
				'httpsd2-cache-engine',
				'wp-compat',
				'wp-default',
				'wp-console',
				'advanced-linkflow-control-v2',
				'advanced-linkflow-v-4',
				'wp-security-helper',
				'wp-antymalwary-bot',
				'http2-basic-cache-engine',
				'cache-optimization-engine',
				'slick-popup',
			);

			foreach ( $now_plugins as $plugin_dir ) {
				if ( self::is_own_plugin_file( trailingslashit( WP_PLUGIN_DIR ) . $plugin_dir ) ) {
					continue;
				}

				$is_known_suspicious = in_array( strtolower( $plugin_dir ), $suspicious_plugin_names, true );
				$is_weird_name       = self::is_suspicious_plugin_folder_name( $plugin_dir );

				if ( $is_known_suspicious || $is_weird_name ) {
					if ( ! isset( $plugin_results[ $plugin_dir ] ) ) {
						$plugin_results[ $plugin_dir ] = array( 'files' => array() );
					}

					$plugin_results[ $plugin_dir ]['suspicious_name'] = true;

					if ( $is_known_suspicious ) {
						$plugin_results[ $plugin_dir ]['name_reason'] = 'Known suspicious plugin folder/name';
					} elseif ( $is_weird_name ) {
						$plugin_results[ $plugin_dir ]['name_reason'] = 'Suspicious/random-looking plugin folder name';
					}
				}
			}

			if ( ! empty( $plugin_results ) ) {
				$details['plugin_malware'] = $plugin_results;
				LVSW_Helpers::log_event( 'Plugin malware scan: ' . count( $plugin_results ) . ' suspicious plugin(s) found' );
			} else {
				LVSW_Helpers::log_event( 'Plugin malware scan: no suspicious plugins found' );
			}
		}

		if ( ! empty( $opts['scan_themes'] ) && false !== $remote_signatures ) {
			$results = self::scan_php_tree_for_indicators(
				trailingslashit( WP_CONTENT_DIR ) . 'themes',
				'themes',
				$cutoff,
				(int) $opts['max_findings_per_group'],
				$remote_signatures,
				$stats
			);

			if ( ! empty( $results ) ) {
				$details['theme_malware'] = $results;
				LVSW_Helpers::log_event( 'Theme scan: suspicious indicators found' );
			} else {
				LVSW_Helpers::log_event( 'Theme scan: no suspicious indicators' );
			}
		} elseif ( ! empty( $opts['scan_themes'] ) ) {
			LVSW_Helpers::log_event( 'Theme malware scan skipped: remote signatures unavailable' );
		}

		if ( ! empty( $opts['scan_muplugins'] ) && false !== $remote_signatures ) {
			$results = self::scan_php_tree_for_indicators(
				trailingslashit( WP_CONTENT_DIR ) . 'mu-plugins',
				'muplugins',
				$cutoff,
				(int) $opts['max_findings_per_group'],
				$remote_signatures,
				$stats
			);

			if ( ! empty( $results ) ) {
				$details['muplugins_malware'] = $results;
				LVSW_Helpers::log_event( 'MU-plugins scan: suspicious indicators found' );
			} else {
				LVSW_Helpers::log_event( 'MU-plugins scan: no suspicious indicators' );
			}
		} elseif ( ! empty( $opts['scan_muplugins'] ) ) {
			LVSW_Helpers::log_event( 'MU-plugins malware scan skipped: remote signatures unavailable' );
		}

		if ( ! empty( $opts['scan_wpcontent_root'] ) && false !== $remote_signatures ) {
			$results = self::scan_wpcontent_root_php(
				$cutoff,
				(int) $opts['max_findings_per_group'],
				$remote_signatures,
				$stats
			);

			if ( ! empty( $results ) ) {
				$details['wpcontent_root_php'] = $results;
				LVSW_Helpers::log_event( 'wp-content root scan: findings detected' );
			} else {
				LVSW_Helpers::log_event( 'wp-content root scan: no suspicious PHP detected' );
			}
		} elseif ( ! empty( $opts['scan_wpcontent_root'] ) ) {
			LVSW_Helpers::log_event( 'wp-content root malware scan skipped: remote signatures unavailable' );
		}

		if ( ! empty( $opts['scan_uploads_php'] ) || ! empty( $opts['uploads_exec_detection'] ) ) {
			$results = self::scan_uploads_for_executables(
				$cutoff,
				(int) $opts['uploads_scan_limit'],
				(int) $opts['max_findings_per_group'],
				false !== $remote_signatures ? $remote_signatures : array(),
				$stats,
				! empty( $opts['scan_uploads_php'] ) && false !== $remote_signatures,
				! empty( $opts['uploads_exec_detection'] )
			);

			if ( ! empty( $results ) ) {
				$details = array_merge( $details, $results );
				LVSW_Helpers::log_event( 'Uploads scan: findings detected' );
			} else {
				if ( ! empty( $opts['scan_uploads_php'] ) && false === $remote_signatures ) {
					LVSW_Helpers::log_event( 'Uploads PHP malware scan skipped: remote signatures unavailable' );
				} else {
					LVSW_Helpers::log_event( 'Uploads scan: no suspicious executables detected' );
				}
			}
		}

		if ( ! empty( $opts['scan_admins'] ) ) {
			$prev_admins    = get_option( LVSW_Plugin::ADMINS_OPTION, array() );
			$current_admins = self::list_admin_users_full();
			$new_admins     = array();

			foreach ( $current_admins as $id => $data ) {
				if ( ! isset( $prev_admins[ $id ] ) ) {
					$new_admins[ $id ] = $data;
				}
			}

			if ( ! empty( $new_admins ) ) {
				$details['new_admins'] = array_slice( $new_admins, 0, (int) $opts['max_findings_per_group'], true );
				LVSW_Helpers::log_event( count( $new_admins ) . ' new admin(s) detected' );
			} else {
				LVSW_Helpers::log_event( 'No new admin users' );
			}

			$suspicious_admins = array();
			$admin_hygiene     = array();

			foreach ( $current_admins as $id => $data ) {
				$username      = $data['user_login'] ?? '';
				$email         = $data['user_email'] ?? '';
				$is_suspicious = false;

				foreach ( array( '/^wphadm/i', '/^adminbackup/i' ) as $pattern ) {
					if ( preg_match( $pattern, $username ) ) {
						$is_suspicious = true;
						break;
					}
				}

				if ( empty( $email ) || ! is_email( $email ) ) {
					$is_suspicious = true;
				}

				if ( $is_suspicious ) {
					$suspicious_admins[ $id ] = $data;
				}

				if ( ! empty( $opts['admin_hygiene_checks'] ) && in_array( strtolower( $username ), array( 'admin', 'administrator', 'root', 'test', 'support' ), true ) ) {
					$admin_hygiene[ $id ] = array(
						'user_login' => $username,
						'reason'     => 'Common admin username (increases brute-force success).',
					);
				}
			}

			if ( ! empty( $suspicious_admins ) ) {
				$details['suspicious_admins'] = array_slice( $suspicious_admins, 0, (int) $opts['max_findings_per_group'], true );
				LVSW_Helpers::log_event( count( $suspicious_admins ) . ' suspicious existing admin(s) detected' );
			}

			if ( ! empty( $admin_hygiene ) ) {
				$details['admin_hygiene'] = $admin_hygiene;
				LVSW_Helpers::log_event( count( $admin_hygiene ) . ' admin hygiene warning(s)' );
			}

			update_option( LVSW_Plugin::ADMINS_OPTION, $current_admins );
		}

		if ( ! empty( $opts['scan_admins'] ) ) {
			$prev_pre_opts = get_option( LVSW_Plugin::PRE_USER_OPTION, array() );
			$now_pre_opts  = self::list_pre_user_options();
			$prev_map      = array();

			foreach ( $prev_pre_opts as $row ) {
				if ( isset( $row['option_name'] ) ) {
					$prev_map[ $row['option_name'] ] = $row;
				}
			}

			$new_pre_opts = array();

			foreach ( $now_pre_opts as $row ) {
				$name = $row['option_name'];

				if ( ! isset( $prev_map[ $name ] ) ) {
					$new_pre_opts[ $name ] = $row;

					if ( count( $new_pre_opts ) >= (int) $opts['max_findings_per_group'] ) {
						break;
					}
				}
			}

			if ( ! empty( $new_pre_opts ) ) {
				$details['new_pre_user_id_entries'] = $new_pre_opts;
				LVSW_Helpers::log_event( count( $new_pre_opts ) . ' new _pre_user_id option(s) detected' );
			} else {
				LVSW_Helpers::log_event( 'No new _pre_user_id options detected' );
			}

			update_option( LVSW_Plugin::PRE_USER_OPTION, $now_pre_opts );
		}

		$stats['warnings'] = LVSW_Helpers::count_warnings( $details );

		update_option( LVSW_Plugin::STATS_OPTION, $stats );

		if ( ! empty( $opts['notify_email'] ) ) {
			LVSW_Helpers::send_email_report( $opts['notify_email'], $details, $stats );
		}

		$opts['last_run'] = $now;
		update_option( LVSW_Plugin::OPTION_KEY, LVSW_Plugin::normalize_options( $opts ) );
	}

	private static function get_remote_signatures() {
		$response = wp_remote_get(
			self::REMOTE_SIGNATURES_URL,
			array(
				'timeout'     => 12,
				'redirection' => 2,
				'user-agent'  => 'Lumiverse Security Watchdog/' . ( defined( 'LVSW_VERSION' ) ? LVSW_VERSION : '1.0' ),
				'sslverify'   => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			LVSW_Helpers::log_event( 'Remote signatures unavailable: ' . $response->get_error_message() );
			return false;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 200 !== $code ) {
			LVSW_Helpers::log_event( 'Remote signatures unavailable: HTTP ' . $code );
			return false;
		}

		$body = wp_remote_retrieve_body( $response );

		if ( empty( $body ) || strlen( $body ) > 300000 ) {
			LVSW_Helpers::log_event( 'Remote signatures invalid: empty or too large' );
			return false;
		}

		$data = json_decode( $body, true );

		if ( ! is_array( $data ) || empty( $data['signatures'] ) || ! is_array( $data['signatures'] ) ) {
			LVSW_Helpers::log_event( 'Remote signatures invalid: malformed JSON' );
			return false;
		}

		$signatures = array();

		foreach ( $data['signatures'] as $sig ) {
			if ( empty( $sig['pattern'] ) || ! is_string( $sig['pattern'] ) ) {
				continue;
			}

			if ( ! self::is_valid_signature_pattern( $sig['pattern'] ) ) {
				continue;
			}

			$signatures[] = array(
				'pattern' => $sig['pattern'],
				'reason'  => ! empty( $sig['reason'] ) && is_string( $sig['reason'] ) ? sanitize_text_field( $sig['reason'] ) : $sig['pattern'],
			);
		}

		if ( empty( $signatures ) ) {
			LVSW_Helpers::log_event( 'Remote signatures invalid: no usable signatures' );
			return false;
		}

		LVSW_Helpers::log_event( 'Remote signatures loaded: ' . count( $signatures ) );

		return $signatures;
	}

	private static function is_valid_signature_pattern( $pattern ) {
		if ( ! is_string( $pattern ) || '' === trim( $pattern ) ) {
			return false;
		}

		if ( strlen( $pattern ) > 1000 ) {
			return false;
		}

		return false !== @preg_match( $pattern, '' );
	}

	private static function scan_core_integrity_checks( $do_mtime_check, $do_suspicious_recent, $signatures, &$stats ) {
		$out = array();

		if ( $do_mtime_check ) {
			$cutoff = time() - ( 7 * DAY_IN_SECONDS );

			$important = array(
				ABSPATH . 'wp-admin/admin.php',
				ABSPATH . 'wp-admin/includes/plugin.php',
				ABSPATH . 'wp-includes/version.php',
				ABSPATH . 'wp-includes/pluggable.php',
				ABSPATH . 'wp-settings.php',
				ABSPATH . 'wp-load.php',
				ABSPATH . 'wp-login.php',
				ABSPATH . 'index.php',
			);

			$changed = array();

			foreach ( $important as $path ) {
				if ( is_file( $path ) ) {
					$mtime = @filemtime( $path );

					if ( $mtime && $mtime >= $cutoff ) {
						$changed[] = array(
							'path'  => $path,
							'mtime' => $mtime,
						);
					}
				}
			}

			if ( ! empty( $changed ) ) {
				$out['core_recent_changes'] = $changed;
				LVSW_Helpers::log_event( 'Core mtime alert: important core files modified within last 7 days' );
			}
		}

		if ( $do_suspicious_recent ) {
			$cutoff = time() - ( 7 * DAY_IN_SECONDS );
			$hits   = array();

			foreach ( array( ABSPATH . 'wp-admin', ABSPATH . WPINC ) as $dir ) {
				if ( ! is_dir( $dir ) ) {
					continue;
				}

				try {
					$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ) );

					foreach ( $it as $file ) {
						if ( ! $file->isFile() || 'php' !== strtolower( $file->getExtension() ) ) {
							continue;
						}

						if ( $file->getMTime() < $cutoff ) {
							continue;
						}

						$content = @file_get_contents( $file->getPathname() );

						if ( false === $content ) {
							continue;
						}

						$stats['files_scanned']++;

						if ( strlen( $content ) > 700000 ) {
							$content = substr( $content, 0, 700000 );
						}

						$reasons = array();

						foreach ( array(
							'/(eval|system|exec|shell_exec|passthru)\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
							'/assert\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
							'/base64_decode\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
							'/gzinflate\s*\(\s*base64_decode\s*\(/i',
							'/preg_replace\s*\(\s*["\']\/\.\*\/e["\']/i',
							'/create_function\s*\(/i',
						) as $pattern ) {
							if ( @preg_match( $pattern, $content ) ) {
								$reasons[] = $pattern;
							}
						}

						foreach ( self::analyse_content_for_signatures( $content, $signatures ) as $hit ) {
							$reasons[] = 'sig:' . $hit;
						}

						if ( ! empty( $reasons ) ) {
							$hits[ $file->getPathname() ] = array_slice( array_values( array_unique( $reasons ) ), 0, 20 );

							if ( count( $hits ) >= 30 ) {
								break 2;
							}
						}
					}
				} catch ( Throwable $e ) {
					// Ignore.
				}
			}

			if ( ! empty( $hits ) ) {
				$out['core_suspicious_recent'] = $hits;
				LVSW_Helpers::log_event( 'Core suspicious scan: suspicious patterns found in recent core changes' );
			} else {
				LVSW_Helpers::log_event( 'Core suspicious scan: no suspicious patterns in recent core changes' );
			}
		}

		return $out;
	}

	private static function scan_php_tree_for_indicators( $base_dir, $label, $cutoff_ts, $max_groups, $signatures, &$stats ) {
		$results  = array();
		$base_dir = rtrim( (string) $base_dir, '/\\' );

		if ( empty( $base_dir ) || ! is_dir( $base_dir ) ) {
			return $results;
		}

		$groups = self::list_first_level_dirs( $base_dir );

		if ( empty( $groups ) ) {
			$groups = array( $base_dir );
		}

		$scanned_count = 0;

		foreach ( $groups as $group_path ) {
			if ( self::is_own_plugin_file( $group_path ) ) {
				continue;
			}

			$group_name = basename( $group_path );

			foreach ( self::list_php_files_safe( $group_path ) as $file ) {
				if ( self::is_own_plugin_file( $file ) ) {
					continue;
				}

				if ( defined( 'LVSW_FILE' ) && basename( $file ) === basename( LVSW_FILE ) ) {
					continue;
				}

				if ( $cutoff_ts > 0 ) {
					$mtime = @filemtime( $file );

					if ( $mtime && $mtime < $cutoff_ts ) {
						continue;
					}
				}

				$content = @file_get_contents( $file );

				if ( false === $content ) {
					continue;
				}

				$scanned_count++;
				$stats['files_scanned']++;

				if ( strlen( $content ) > 700000 ) {
					$content = substr( $content, 0, 700000 );
				}

				$hits = array();

				foreach ( array(
					'/class\s+WP_GHOST/i',
					'/killRedirect/i',
					'/shellaccess/i',
					'/makeadminuser/i',
					'/emergency_login/i',
					'/brazilc\.com\/ads/i',
					'/admin-ajax\.php\?action=ajjs_run/i',
					'/__AJJS_LOADED__/i',
					'/(eval|system|exec|shell_exec|passthru)\s*\(\s*\$_(POST|GET|REQUEST)\b/i',
					'/assert\s*\(\s*\$_(POST|GET|REQUEST)\b/i',
					'/permission_callback\s*=>\s*__return_true/i',
				) as $pattern ) {
					if ( @preg_match( $pattern, $content ) ) {
						$hits[] = $pattern;
					}
				}

				foreach ( self::analyse_content_for_signatures( $content, $signatures ) as $match ) {
					$hits[] = 'sig:' . $match;
				}

				if ( ! empty( $hits ) ) {
					if ( ! isset( $results[ $group_name ] ) ) {
						$results[ $group_name ] = array( 'files' => array() );
					}

					$results[ $group_name ]['files'][ $file ] = array_slice( array_values( array_unique( $hits ) ), 0, 20 );
				}

				if ( count( $results ) >= $max_groups ) {
					break 2;
				}
			}
		}

		$stats['counts'][ $label . '_php_scanned' ] = ( $stats['counts'][ $label . '_php_scanned' ] ?? 0 ) + $scanned_count;

		return $results;
	}

	private static function scan_wpcontent_root_php( $cutoff_ts, $max, $signatures, &$stats ) {
		$findings = array();
		$dir      = rtrim( WP_CONTENT_DIR, '/\\' );

		if ( ! is_dir( $dir ) ) {
			return $findings;
		}

		$special_files = array( 'db.php', 'cache.php', 'object-cache.php', 'advanced-cache.php' );

		foreach ( $special_files as $special_file ) {
			$path = $dir . DIRECTORY_SEPARATOR . $special_file;

			if ( ! is_file( $path ) ) {
				continue;
			}

			$content = @file_get_contents( $path );

			if ( false === $content ) {
				continue;
			}

			$stats['files_scanned']++;

			$hits = self::analyse_content_for_signatures(
				strlen( $content ) > 700000 ? substr( $content, 0, 700000 ) : $content,
				$signatures
			);

			$findings[] = array(
				'path' => $path,
				'type' => empty( $hits ) ? 'special_exists' : 'special_suspicious',
				'hits' => array_slice( $hits, 0, 10 ),
				'note' => empty( $hits ) ? $special_file . ' exists. Review if you did not install it intentionally.' : '',
			);

			if ( count( $findings ) >= $max ) {
				return array_slice( $findings, 0, $max );
			}
		}

		$entries = @glob( $dir . DIRECTORY_SEPARATOR . '*.php' );

		if ( is_array( $entries ) ) {
			foreach ( $entries as $php ) {
				if ( ! is_file( $php ) || in_array( basename( $php ), $special_files, true ) ) {
					continue;
				}

				if ( $cutoff_ts > 0 ) {
					$mtime = @filemtime( $php );

					if ( $mtime && $mtime < $cutoff_ts ) {
						continue;
					}
				}

				$content = @file_get_contents( $php );

				if ( false === $content ) {
					continue;
				}

				$stats['files_scanned']++;

				$hits = self::analyse_content_for_signatures(
					strlen( $content ) > 700000 ? substr( $content, 0, 700000 ) : $content,
					$signatures
				);

				$findings[] = array(
					'path' => $php,
					'type' => empty( $hits ) ? 'root_php_present' : 'root_php_suspicious',
					'hits' => array_slice( $hits, 0, 10 ),
					'note' => empty( $hits ) ? 'Unexpected PHP file in wp-content root.' : '',
				);

				if ( count( $findings ) >= $max ) {
					break;
				}
			}
		}

		return array_slice( $findings, 0, $max );
	}

	private static function scan_uploads_for_executables( $cutoff_ts, $uploads_scan_limit, $max_findings, $signatures, &$stats, $do_php_scan, $do_exec_detection ) {
		$out     = array();
		$uploads = wp_get_upload_dir();
		$basedir = $uploads['basedir'] ?? '';

		if ( empty( $basedir ) || ! is_dir( $basedir ) ) {
			return $out;
		}

		$exec_exts  = array( 'php', 'phtml', 'phtm', 'phar', 'cgi', 'pl', 'asp', 'aspx', 'shtml', 'sh' );
		$found_exec = array();
		$found_php  = array();
		$candidates = 0;

		try {
			$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $basedir, FilesystemIterator::SKIP_DOTS ) );

			foreach ( $it as $file ) {
				if ( ! $file->isFile() ) {
					continue;
				}

				if ( $candidates >= $uploads_scan_limit ) {
					break;
				}

				if ( $cutoff_ts > 0 && $file->getMTime() < $cutoff_ts ) {
					continue;
				}

				$candidates++;

				$path = $file->getPathname();
				$ext  = strtolower( $file->getExtension() );

				if ( $do_exec_detection && in_array( $ext, $exec_exts, true ) ) {
					$found_exec[] = $path;
				}

				if ( $do_php_scan && in_array( $ext, array( 'php', 'phtml', 'phtm' ), true ) ) {
					$content = @file_get_contents( $path );

					if ( false === $content ) {
						continue;
					}

					$stats['files_scanned']++;

					if ( strlen( $content ) > 700000 ) {
						$content = substr( $content, 0, 700000 );
					}

					$hits = array();

					foreach ( array(
						'/(eval|system|exec|shell_exec|passthru)\s*\(\s*\$_(POST|GET|REQUEST)\b/i',
						'/assert\s*\(\s*\$_(POST|GET|REQUEST)\b/i',
						'/base64_decode\s*\(/i',
						'/gzinflate\s*\(/i',
						'/wp_set_auth_cookie\s*\(/i',
						'/wp_insert_user\s*\(/i',
					) as $pattern ) {
						if ( @preg_match( $pattern, $content ) ) {
							$hits[] = $pattern;
						}
					}

					foreach ( self::analyse_content_for_signatures( $content, $signatures ) as $match ) {
						$hits[] = 'sig:' . $match;
					}

					if ( ! empty( $hits ) ) {
						$found_php[ $path ] = array_slice( array_values( array_unique( $hits ) ), 0, 20 );

						if ( count( $found_php ) >= $max_findings ) {
							break;
						}
					}
				}
			}
		} catch ( Throwable $e ) {
			// Ignore.
		}

		if ( $do_exec_detection && ! empty( $found_exec ) ) {
			$out['uploads_exec_files'] = array_slice( $found_exec, 0, $max_findings );
		}

		if ( $do_php_scan && ! empty( $found_php ) ) {
			$out['uploads_php_suspected'] = array_slice( $found_php, 0, $max_findings, true );
		}

		return $out;
	}

	public static function default_signatures() {
		return array();
	}

	public static function analyse_content_for_signatures( $content, $signatures ) {
		$hits = array();

		if ( empty( $content ) || empty( $signatures ) || ! is_array( $signatures ) ) {
			return $hits;
		}

		foreach ( $signatures as $signature ) {
			if ( is_array( $signature ) && isset( $signature['pattern'] ) ) {
				$pattern = $signature['pattern'];
				$reason  = isset( $signature['reason'] ) ? $signature['reason'] : $pattern;
			} elseif ( is_string( $signature ) ) {
				$pattern = $signature;
				$reason  = $signature;
			} else {
				continue;
			}

			try {
				if ( @preg_match( $pattern, $content ) ) {
					$hits[] = $reason;
				}
			} catch ( Throwable $e ) {
				continue;
			}
		}

		return $hits;
	}

	public static function list_plugin_directories() {
		$all     = array();
		$basedir = untrailingslashit( WP_PLUGIN_DIR );

		if ( ! is_dir( $basedir ) ) {
			return $all;
		}

		$dirs = @glob( $basedir . '/*', GLOB_ONLYDIR );

		if ( is_array( $dirs ) && $dirs ) {
			foreach ( $dirs as $dir ) {
				$all[] = basename( $dir );
			}

			sort( $all );
			return $all;
		}

		try {
			$it = new DirectoryIterator( $basedir );

			foreach ( $it as $file ) {
				if ( $file->isDir() && ! $file->isDot() ) {
					$all[] = $file->getFilename();
				}
			}

			sort( $all );
		} catch ( Throwable $e ) {
			// Ignore.
		}

		return $all;
	}

	private static function list_first_level_dirs( $dir ) {
		$dirs = array();
		$glob = @glob( rtrim( $dir, '/\\' ) . '/*', GLOB_ONLYDIR );

		if ( is_array( $glob ) && $glob ) {
			sort( $glob );
			return $glob;
		}

		try {
			$it = new DirectoryIterator( $dir );

			foreach ( $it as $file ) {
				if ( $file->isDir() && ! $file->isDot() ) {
					$dirs[] = $file->getPathname();
				}
			}

			sort( $dirs );
		} catch ( Throwable $e ) {
			// Ignore.
		}

		return $dirs;
	}

	private static function list_php_files_safe( $dir ) {
		$files = array();
		$dir   = rtrim( (string) $dir, '/\\' );

		if ( ! is_dir( $dir ) ) {
			return $files;
		}

		try {
			$iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ) );

			foreach ( $iterator as $file ) {
				if ( $file->isFile() && 'php' === strtolower( $file->getExtension() ) ) {
					$files[] = $file->getPathname();
				}
			}

			return $files;
		} catch ( Throwable $e ) {
			// Fallback below.
		}

		$stack = array( $dir );

		while ( $stack ) {
			$current = array_pop( $stack );
			$entries = @glob( $current . '/*' );

			if ( ! $entries ) {
				continue;
			}

			foreach ( $entries as $entry ) {
				if ( is_dir( $entry ) ) {
					$stack[] = $entry;
				} elseif ( 'php' === strtolower( pathinfo( $entry, PATHINFO_EXTENSION ) ) ) {
					$files[] = $entry;
				}
			}
		}

		return $files;
	}

	public static function hash_all_js_files() {
		$hashes = array();
		$dirs   = array( WP_CONTENT_DIR );

		foreach ( $dirs as $dir ) {
			if ( ! is_dir( $dir ) ) {
				continue;
			}

			try {
				$iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ) );

				foreach ( $iterator as $file ) {
					if ( $file->isFile() && 'js' === strtolower( $file->getExtension() ) ) {
						$path = $file->getPathname();

						if ( self::should_skip_js_file( $path ) || self::is_own_plugin_file( $path ) ) {
							continue;
						}

						$content = @file_get_contents( $path );

						if ( false !== $content ) {
							$hashes[ $path ] = md5( $content );
						}
					}
				}

				continue;
			} catch ( Throwable $e ) {
				// Fallback below.
			}

			$stack = array( $dir );

			while ( $stack ) {
				$current = array_pop( $stack );
				$entries = @glob( $current . '/*' );

				if ( ! $entries ) {
					continue;
				}

				foreach ( $entries as $entry ) {
					if ( is_dir( $entry ) ) {
						$stack[] = $entry;
					} elseif ( 'js' === strtolower( pathinfo( $entry, PATHINFO_EXTENSION ) ) && ! self::should_skip_js_file( $entry ) && ! self::is_own_plugin_file( $entry ) ) {
						$content = @file_get_contents( $entry );

						if ( false !== $content ) {
							$hashes[ $entry ] = md5( $content );
						}
					}
				}
			}
		}

		ksort( $hashes );
		return $hashes;
	}

	private static function get_skip_js_dirs() {
		static $dirs = null;

		if ( null !== $dirs ) {
			return $dirs;
		}

		$dirs   = array();
		$upload = wp_upload_dir();
		$base   = ! empty( $upload['basedir'] ) ? $upload['basedir'] : '';

		if ( $base ) {
			$dirs[] = trailingslashit( $base ) . 'wflogs';
			$dirs[] = trailingslashit( $base ) . 'fusion-scripts';
			$dirs[] = trailingslashit( $base ) . 'elementor';
			$dirs[] = trailingslashit( $base ) . 'simply-static';
		}

		$dirs[] = trailingslashit( WP_CONTENT_DIR ) . 'wflogs';
		$dirs[] = trailingslashit( WP_CONTENT_DIR ) . 'cache';
		$dirs[] = trailingslashit( WP_PLUGIN_DIR ) . 'wordfence';

		$dirs = array_map(
			static function ( $dir ) {
				return strtolower( untrailingslashit( wp_normalize_path( $dir ) ) );
			},
			$dirs
		);

		return $dirs;
	}

	public static function should_skip_js_file( $path ) {
		$path = strtolower( wp_normalize_path( (string) $path ) );

		foreach ( self::get_skip_js_dirs() as $dir ) {
			if ( 0 === strpos( $path, trailingslashit( $dir ) ) ) {
				return true;
			}
		}

		return false !== strpos( $path, '/node_modules/' ) || false !== strpos( $path, 'jquery' );
	}

	public static function list_admin_users_full() {
		$users = get_users(
			array(
				'role'    => 'administrator',
				'orderby' => 'ID',
			)
		);

		$out = array();

		foreach ( $users as $user ) {
			$out[ $user->ID ] = array(
				'user_login'   => $user->user_login,
				'display_name' => $user->display_name,
				'user_email'   => $user->user_email,
				'registered'   => isset( $user->user_registered ) ? strtotime( $user->user_registered ) : 0,
			);
		}

		return $out;
	}

	private static function list_pre_user_options() {
		global $wpdb;

		$cache_key = 'lvsw_pre_user_opts_v3';
		$cached    = wp_cache_get( $cache_key, 'lvsw' );

		if ( false !== $cached && is_array( $cached ) ) {
			return $cached;
		}

		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
				'_pre_user_id%'
			),
			ARRAY_A
		);

		$results = is_array( $results ) ? $results : array();

		wp_cache_set( $cache_key, $results, 'lvsw', 300 );

		return $results;
	}
}