<?php

namespace WPSecurityNinja\Plugin;

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

define( 'WF_SN_MS_OPTIONS', 'wf_sn_ms_options' );
define( 'WF_SN_MS_CACHE', 'wf_sn_ms_cache' );
define( 'WF_SN_MS_INTEGRITY_RESULTS', 'wf_sn_ms_integrity_results' );
define( 'WF_SN_MS_RESULTS', 'wf_sn_ms_results' );
define( 'WF_SN_MS_WHITELIST', 'wf_sn_ms_whitelist' );
define( 'WF_SN_MS_DELETELIST', 'wf_sn_ms_deletelist' );
define( 'WF_SN_MS_SCAN_TOTAL_TIME', 'wf_sn_ms_scan_total_time' );
define( 'WF_SN_MS_SK', 'wf_sn_ms_sk' ); // Secret key
define( 'WF_SN_MS_IV', 'wf_sn_ms_iv' ); // IV
define( 'WF_SN_MS_LAST_PATTERN_UPDATE', 'wf_sn_ms_last_pattern_update' ); // When was last updated

class Wf_Sn_Ms {




	/**
	 * get_whitelist.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  mixed
	 */
	/**
	 * Get legacy whitelist patterns (for backward compatibility)
	 * This function now returns patterns from the new advanced whitelist system
	 *
	 * @return array Array of whitelist patterns
	 */
	public static function get_whitelist() {
		// Define the same patterns as in the advanced whitelist system
		// This avoids the need to instantiate the MalwareScanner class
		$internal_whitelist = array(
			'*.DS_Store',
			'*/js_composer/vc_classmap.json.php',
			'*/plugins/pretty-link/pro/*',
			'*/Divi/includes/builder/frontend-builder/helpers.php',
			'*/wp-snapshots/*',
			'*/plugins/jetpack/*',
			'*/uploads/backupbuddy_temp/*',
			'*/uploads/pb_backupbuddy/*',
			'*/plugins/security-ninja/*',
			'*/plugins/security-ninja-premium/*',
			'*/plugins/seo-booster-premium/*',
			'*/plugins/backupbuddy/*',
			'*/plugins/wp-spamshield/*',
			'*/plugins/login-ninja/*',
			'*/plugins/gravityforms/gravityforms.php',
			'*/plugins/5sec-google-authenticator/*',
			'*/plugins/google-maps-widget/*',
			'*/plugins/optin-ninja/*',
			'*/plugins/smartmonitor/*',
			'*/plugins/under-construction-page/*',
			'*/plugins/profit_builder/*',
			'*/plugins/leadsflow-pro/*',
			'*/wflogs/*',
			'*/themes/enfold/includes/admin/demo_files/*',
			'*/freemius/templates/secure-https-header.php',
			'*/themes/enfold/css/dynamic-css.php',
			'*/tcpdf/tcpdf_barcodes_1d.php',
			'*/plugins/wp-seo-keyword-optimizer-premium/classes/class.import.php',
			'*/phpseclib/Crypt/*',
			'*/guzzle/src/*',
			'*/dompdf/lib/fonts/*',
			'*/plugins/wp-seo-keyword-optimizer-premium/admin/controller/controller.php',
			'*/plugins/wp-reset/*',
			'*/plugins/minimal-coming-soon-maintenance-mode/*',
			'*/plugins/wp-seopress-pro/*',
			'*/plugins/clsop/inc/Engine/Optimization/DeferJS/DeferJS.php',
		);

		// Apply the same filter as before for backward compatibility
		$internal_whitelist = apply_filters( 'securityninja_whitelist', $internal_whitelist );

		return $internal_whitelist;
	}

	/**
	 * Get flat list of path/pattern strings from user whitelist (per-file paths + pattern entries).
	 * Used for setIgnore() and path checks.
	 *
	 * @return array List of path or pattern strings.
	 */
	public static function get_whitelist_paths_and_patterns() {
		$whitelist = get_option( WF_SN_MS_WHITELIST, array() );
		$paths     = array();
		if ( is_array( $whitelist ) ) {
			foreach ( $whitelist as $item ) {
				if ( ! is_array( $item ) ) {
					continue;
				}
				if ( isset( $item['pattern'] ) && $item['pattern'] !== '' ) {
					$paths[] = $item['pattern'];
				} elseif ( ! empty( $item['filename'] ) ) {
					$paths[] = $item['filename'];
				}
			}
		}
		return apply_filters( 'securityninja_malware_exclude_paths', $paths );
	}


	/**
	 * Schedule cron jobs
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function schedule_cron_jobs() {
		// Update GEOIP database - once a month
		if ( ! wp_next_scheduled( 'secnin_update_samples' ) ) {
			wp_schedule_event( time(), 'monthly', 'secnin_update_samples' );
		}
	}



	/**
	 * Encrypt and decrypt - Modified
	 * Stores both key and IV in options table - no further need for hiding details.
	 *
	 * @author  Nazmul Ahsan <n.mukto@gmail.com>
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $string string to be encrypted/decrypted
	 * @param   string  $action what to do with this? e for encrypt, d for decrypt
	 * @return  mixed
	 */
	public static function string_crypt( $string, $action = 'e' ) {

		$wf_sn_ms_sk = get_option( WF_SN_MS_SK, false );
		if ( ! $wf_sn_ms_sk ) {
			$wf_sn_ms_sk = base64_encode( \openssl_random_pseudo_bytes( 32 ) );
			update_option( WF_SN_MS_SK, $wf_sn_ms_sk, false );
		}

		$wf_sn_ms_iv = get_option( WF_SN_MS_IV, false );
		if ( ! $wf_sn_ms_iv ) {
			$wf_sn_ms_iv = \openssl_random_pseudo_bytes( \openssl_cipher_iv_length( 'aes-256-cbc' ) );
			$wf_sn_ms_iv = substr( hash( 'sha256', $wf_sn_ms_iv ), 0, 16 );
			update_option( WF_SN_MS_IV, $wf_sn_ms_iv, false );
		}

		$output = false;

		if ( 'e' === $action ) {
			$output = base64_encode( \openssl_encrypt( $string, 'aes-256-cbc', $wf_sn_ms_sk, 0, $wf_sn_ms_iv ) );
		} elseif ( 'd' === $action ) {
			$output = \openssl_decrypt( base64_decode( $string ), 'aes-256-cbc', $wf_sn_ms_sk, 0, $wf_sn_ms_iv );
		}

		return $output;
	}





	/**
	 * Returns status of scan stored in an option
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  boolean
	 */
	public static function get_scan_status() {
		check_ajax_referer( 'sn_ms' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}
		$wf_sn_ms_results = get_option( WF_SN_MS_RESULTS );
		if ( isset( $wf_sn_ms_results['do_mal_scan'] ) ) {
			wp_send_json_success( $wf_sn_ms_results['do_mal_scan'] );
		} else {
			wp_send_json_error( __( 'No results', 'security-ninja' ) );
		}
		return false;
	}

	/**
	 * AJAX handler for scan progress (current path being scanned).
	 */
	public static function ajax_scan_progress() {
		check_ajax_referer( 'sn_ms' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}
		require_once __DIR__ . '/class-wf-sn-ms-util.php';
		wp_send_json_success( Wf_Sn_Ms_Util::get_scan_progress() );
	}



	/**
	 *
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function get_or_update_samples() {
		// Checks if updated recently and bails if so.
		$last_update = get_option( WF_SN_MS_LAST_PATTERN_UPDATE );
		if ( $last_update ) {
			$current_time = time();
			$seconds_diff = $current_time - $last_update;
			if ( $seconds_diff < DAY_IN_SECONDS ) {
				return true;
			}
		}

		$api_url = 'https://wpsecurityninja.sfo2.cdn.digitaloceanspaces.com/samples.json';

		global $secnin_fs;

		$user = $secnin_fs->get_user();

		if ( ! $user || ! isset( $user->public_key ) ) {
			return false;
		}

		$publickey = $secnin_fs->get_user()->public_key;

		if ( ! $publickey ) {
			return false;
		}

		$request_url = add_query_arg( 'pk', $publickey, $api_url );
		$request_url = add_query_arg( 'ver', wf_sn::$version, $request_url );

		$request = wp_remote_get( $request_url );

		if ( is_wp_error( $request ) ) {
			wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Error getting malware definitions from API.', array( 'response' => $request->get_error_message() ) );
			return false; // Bail early
		}

		$body = wp_remote_retrieve_body( $request );

		$data = json_decode( $body );

		if ( ! empty( $data ) ) {

			if ( ( isset( $data->code ) ) && ( 'rate_limit_exceeded' === $data->code ) ) {
				return false;
			} else {

				global $wp_filesystem;
				if ( empty( $wp_filesystem ) ) {
					require_once ABSPATH . '/wp-admin/includes/file.php';
					WP_Filesystem();
				}
				$upload_dir            = wp_upload_dir();
				$secninja_upload_dir   = $upload_dir['basedir'] . '/security-ninja/';
				$patternsfoldername    = $secninja_upload_dir . 'base64_patterns/';
				$definitionsfoldername = $secninja_upload_dir . 'definitions/';

				if ( ! $wp_filesystem->exists( $secninja_upload_dir ) ) {
					$wp_filesystem->mkdir( $secninja_upload_dir );
				}

				if ( ! $wp_filesystem->exists( $patternsfoldername ) ) {
					$wp_filesystem->mkdir( $patternsfoldername );
				}

				if ( ! $wp_filesystem->exists( $definitionsfoldername ) ) {
					$wp_filesystem->mkdir( $definitionsfoldername );
				}

				$oldfile = $secninja_upload_dir . 'base64_patterns/php_functions.txt';
				if ( file_exists( $oldfile ) ) {
					unlink( $oldfile );
				}
				$oldfile = $secninja_upload_dir . 'base64_patterns/php_keywords.txt';
				if ( file_exists( $oldfile ) ) {
					unlink( $oldfile );
				}
				$oldfile = $secninja_upload_dir . 'definitions/patterns_iraw.txt';
				if ( file_exists( $oldfile ) ) {
					unlink( $oldfile );
				}
				$oldfile = $secninja_upload_dir . 'definitions/patterns_raw.txt';
				if ( file_exists( $oldfile ) ) {
					unlink( $oldfile );
				}
				$oldfile = $secninja_upload_dir . 'definitions/patterns_re.txt';
				if ( file_exists( $oldfile ) ) {
					unlink( $oldfile );
				}

				if ( isset( $data->php_functions ) ) {
					$logfile = $patternsfoldername . 'php_functions.dat';
					// Attempt to write to the file
					$result = $wp_filesystem->put_contents( $logfile, self::string_crypt( $data->php_functions, 'e' ), FS_CHMOD_FILE );
					if ( ! $result ) {
						// Logging with additional context
						$error_message = sprintf( 'Problem storing the php_functions.dat file. Target path: %s', $logfile );

						// Check if the directory is writable
						if ( ! $wp_filesystem->is_writable( dirname( $logfile ) ) ) {
							$error_message .= ' Directory not writable.';
						}

						// Include information about whether the file exists and its write permissions
						if ( $wp_filesystem->exists( $logfile ) && ! $wp_filesystem->is_writable( $logfile ) ) {
							$error_message .= ' File exists but is not writable.';
						} elseif ( ! $wp_filesystem->exists( $logfile ) ) {
							$error_message .= ' File does not exist.';
						}

						// Log the constructed error message
						wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', $error_message );
					}
				}

				if ( isset( $data->php_keywords ) ) {
					$logfile = $patternsfoldername . 'php_keywords.dat';
					if ( ! $wp_filesystem->put_contents( $logfile, self::string_crypt( $data->php_keywords, 'e' ), FS_CHMOD_FILE ) ) {
						wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Problem storing the php_keywords.dat file.' );
					}
				}

				// PATTERNS
				if ( isset( $data->patterns_iraw ) ) {
					$logfile = $definitionsfoldername . 'patterns_iraw.dat';
					if ( ! $wp_filesystem->put_contents( $logfile, self::string_crypt( $data->patterns_iraw, 'e' ), FS_CHMOD_FILE ) ) {
						wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Problem storing the patterns_iraw.dat file.', '' );
					}
				}

				if ( isset( $data->patterns_raw ) ) {
					$logfile = $definitionsfoldername . 'patterns_raw.dat';
					if ( ! $wp_filesystem->put_contents( $logfile, self::string_crypt( $data->patterns_raw, 'e' ), FS_CHMOD_FILE ) ) {
						wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Problem storing the patterns_raw.dat file.' );
					}
				}

				if ( isset( $data->patterns_re ) ) {
					$logfile = $definitionsfoldername . 'patterns_re.dat';
					if ( ! $wp_filesystem->put_contents( $logfile, self::string_crypt( $data->patterns_re, 'e' ), FS_CHMOD_FILE ) ) {
						wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Problem storing the patterns_re.dat file.' );
					}
				}

				$last_update = time();
				update_option( WF_SN_MS_LAST_PATTERN_UPDATE, $last_update, false );
			}
		} else {
			// removes the last update to force routine to update again.
			delete_option( WF_SN_MS_LAST_PATTERN_UPDATE );
			wf_sn_el_modules::log_event( 'security_ninja', 'malware_scanner_definitions_error', 'Empty response from API.' );
		}
	}


	/**
	 * init plugin
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function init() {
		// does the user have enough privilages to use the plugin?
		if ( is_admin() ) {
			// add tab to Security Ninja tabs
			add_filter( 'sn_tabs', array( __NAMESPACE__ . '\\wf_sn_ms', 'sn_tabs' ) );
			add_action( 'secnin_update_samples', array( __NAMESPACE__ . '\\wf_sn_ms', 'get_or_update_samples' ) );
			add_action( 'init', array( __NAMESPACE__ . '\\wf_sn_ms', 'schedule_cron_jobs' ) );
			add_action( 'admin_enqueue_scripts', array( __NAMESPACE__ . '\\wf_sn_ms', 'enqueue_scripts' ) );

			add_action( 'wp_ajax_sn_ms_run_scan', array( __NAMESPACE__ . '\\wf_sn_ms', 'run_scan' ) );
			add_action( 'wp_ajax_sn_ms_scan_progress', array( __NAMESPACE__ . '\\wf_sn_ms', 'ajax_scan_progress' ) );
			add_action( 'wp_ajax_sn_ms_mark_scan_failed', array( __NAMESPACE__ . '\\wf_sn_ms', 'ajax_mark_scan_failed' ) );
			add_action( 'wp_ajax_sn_ms_whitelist_file', array( __NAMESPACE__ . '\\wf_sn_ms', 'add_whitelisted_file' ) );
			add_action( 'wp_ajax_sn_ms_whitelist_all', array( __NAMESPACE__ . '\\wf_sn_ms', 'whitelist_all_suspicious_files' ) );
			add_action( 'wp_ajax_revert_whitelist_btn', array( __NAMESPACE__ . '\\wf_sn_ms', 'revert_whitelisted_file' ) );

			add_action( 'wp_ajax_sn_ms_reset_whitelist', array( __NAMESPACE__ . '\\wf_sn_ms', 'reset_whitelist' ) );
			add_action( 'wp_ajax_sn_ms_reset_results', array( __NAMESPACE__ . '\\wf_sn_ms', 'reset_results' ) );
			add_action( 'wp_ajax_sn_ms_delete_file', array( __NAMESPACE__ . '\\wf_sn_ms', 'delete_file' ) );
			add_action( 'admin_notices', array( __NAMESPACE__ . '\\wf_sn_ms', 'warnings' ) );
			//  add_action('sn_overlay_content', array(__NAMESPACE__ . '\\wf_sn_ms', 'overlay_content'));
		}
	}

	/**
	 * Enqueue CSS and JS scripts on plugin's admin page
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function enqueue_scripts() {
		if ( wf_sn::is_plugin_page() ) {
			$plugin_url = plugin_dir_url( __FILE__ );

			wp_enqueue_style( 'sn-ms-css', $plugin_url . 'css/wf-sn-ms.css', array(), wf_sn::$version );
			wp_enqueue_script( 'sn-ms-js', $plugin_url . 'js/wf-sn-ms.js', array( 'jquery', 'sn-dialog' ), wf_sn::$version, true );

			$js_vars = array(
				'nonce'   => wp_create_nonce( 'sn_ms' ),
				'strings' => array(
					'whitelist_all_confirm' => esc_html__( 'Whitelist all currently flagged files? They will be ignored in future scans until removed from the whitelist.', 'security-ninja' ),
					'whitelist_all_success' => esc_html__( 'All flagged files were added to the whitelist.', 'security-ninja' ),
					'whitelist_all_failed'  => esc_html__( 'Could not whitelist all files. Please try again.', 'security-ninja' ),
					'scan_step_failed'      => esc_html__( 'This scan step failed. The scan is incomplete — do not treat the site as clean. Reload and try again.', 'security-ninja' ),
					'scan_ajax_failed'      => esc_html__( 'The malware scan request failed. The scan is incomplete — do not treat the site as clean. Reload and try again.', 'security-ninja' ),
					'scan_invalid_response' => esc_html__( 'Invalid scan response. The scan is incomplete — do not treat the site as clean. Reload and try again.', 'security-ninja' ),
				),
			);
			wp_localize_script( 'sn-ms-js', 'wf_sn_ms', $js_vars );
		}
	}



	/**
	 * add new tab
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $tabs
	 * @return  mixed
	 */
	public static function sn_tabs( $tabs ) {
		$core_tab = array(
			'id'       => 'sn_malware',
			'class'    => '',
			'label'    => __( 'Malware', 'security-ninja' ),
			'callback' => array( __NAMESPACE__ . '\\wf_sn_ms', 'malware_page' ),
		);
		$done     = 0;
		$count    = self::count_suspicious_files();
		if ( $count > 0 ) {
			$core_tab['count'] = $count;
		}

		for ( $i = 0; $i < count( $tabs ); $i++ ) {
			if ( 'sn_malware' === $tabs[ $i ]['id'] ) {
				$tabs[ $i ] = $core_tab;
				$done       = 1;
				break;
			}
		}

		if ( ! $done ) {
			$tabs[] = $core_tab;
		}

		return $tabs;
	}



	/**
	 * add custom message to overlay
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void

	 */

	/*
	public static function overlay_content()
	{
		echo '<div id="sn-malware-scanner" style="display: none;">';
		echo '<h3>Scanning your website for malware</h3>';

		$wf_sn_ms_scan_total_time = get_option(WF_SN_MS_SCAN_TOTAL_TIME);

		echo '<p><span id="mscounterminutes">00</span>:<span id="mscounterseconds">00</span></p>';

		echo '<ul class="malware-scan-list"></ul>';
		$infolink = Utils::generate_sn_web_link( 'malware_docs', '/docs/malware-scanner/malware-scanner/' );

		if (! \WPSecurityNinja\Plugin\Wf_Sn_Wl::is_active()) {
			echo '<p><small>Wondering what is happening? <a href="' . esc_url($infolink) . '" target="_blank">Click here</a> (opens a new window)</small></p>';
		}

		if (isset($wf_sn_ms_scan_total_time)) {
			$total_scan_time = $wf_sn_ms_scan_total_time;
			if ($total_scan_time) {
				$spenttime = self::seconds2human($total_scan_time);
				echo '<p><small>' . sprintf(esc_html__('Your last malware scan took %s ', 'security-ninja'), $spenttime) . '</small></p>';
			}
		}
		echo '</div><!-- #sn-malware-scanner -->';
	}
	*/




	/**
	 * update_cache.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $data
	 * @param   mixed   $key    Default: null
	 * @return  mixed
	 */
	public static function update_cache( $data, $key = null ) {
		if ( ! isset( $key ) ) {
			$cache = $data;
		} else {
			$cache         = get_option( WF_SN_MS_CACHE );
			$cache[ $key ] = $data;
		}

		return update_option( WF_SN_MS_CACHE, $cache, false );
	}

	/**
	 * retreive cached data
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $key    Default: null
	 * @return  void
	 */
	public static function get_cache( $key = null ) {
		$cache = get_option( WF_SN_MS_CACHE );

		if ( isset( $key ) && isset( $cache[ $key ] ) ) {
			return $cache[ $key ];
		} elseif ( isset( $key ) ) {
			return array();
		} else {
			return $cache;
		}
	} // get_cache

	/**
	 * save results data
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $data
	 * @param   mixed   $key    Default: null
	 * @return  mixed
	 */
	public static function update_results( $data, $key = null ) {
		if ( ! isset( $key ) ) {
			$cache = is_array( $data ) ? $data : array();
		} else {
			$cache = get_option( WF_SN_MS_RESULTS );
			if ( ! is_array( $cache ) ) {
				$cache = array();
			}
			$cache[ $key ] = $data;
		}

		return update_option( WF_SN_MS_RESULTS, $cache, false );
	} // update_results

	/**
	 * Mark malware scan as started (clears findings; does not stamp last_run).
	 *
	 * @since 5.295
	 * @return bool
	 */
	public static function mark_scan_in_progress() {
		return update_option(
			WF_SN_MS_RESULTS,
			array(
				'scan_status' => 'in_progress',
			),
			false
		);
	}

	/**
	 * Mark malware scan as successfully completed.
	 *
	 * @since 5.295
	 * @return bool
	 */
	public static function mark_scan_complete() {
		$cache = get_option( WF_SN_MS_RESULTS );
		if ( ! is_array( $cache ) ) {
			$cache = array();
		}
		$cache['scan_status'] = 'complete';
		$cache['last_run']    = time();
		unset( $cache['failed_at'] );

		return update_option( WF_SN_MS_RESULTS, $cache, false );
	}

	/**
	 * Mark malware scan as failed / incomplete so UI never shows a false clean banner.
	 *
	 * @since 5.295
	 * @return bool
	 */
	public static function mark_scan_failed() {
		$cache = get_option( WF_SN_MS_RESULTS );
		if ( ! is_array( $cache ) ) {
			$cache = array();
		}
		$cache['scan_status'] = 'failed';
		$cache['failed_at']   = time();
		// Do not set last_run — incomplete scans must not look like a finished clean run.
		unset( $cache['last_run'] );

		return update_option( WF_SN_MS_RESULTS, $cache, false );
	}

	/**
	 * AJAX: mark current malware scan as failed after a client-side AJAX error.
	 *
	 * @since 5.295
	 * @return void
	 */
	public static function ajax_mark_scan_failed() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		self::mark_scan_failed();
		wp_send_json_success(
			array(
				'message' => __( 'Scan marked as incomplete.', 'security-ninja' ),
			)
		);
	}

	/**
	 * Run malware scan (same as manual scan). Used by the Scheduler when "all" is selected.
	 *
	 * @return array|false Scan result array or false on failure.
	 */
	public static function run_malware_scan_for_scheduler() {
		require_once __DIR__ . '/class-wf-sn-ms-util.php';
		require_once __DIR__ . '/class-malware-scanner-adapter.php';
		$malscan = new \WPSecurityNinja\Plugin\MalwareScannerAdapter();
		$malscan->setFlagHideOk( true );
		$malscan->setFlagComments( true );
		$malscan->setFlagHideWhitelist( true );
		$malscan->setFlagNoStop( false );
		$malscan->setFlagLineNumber( true );

		$integrity_whitelist = array();
		$integrity_results   = self::get_results( 'do_integrity_scan' );
		if ( $integrity_results && is_array( $integrity_results['validated_plugins'] ?? null ) ) {
			foreach ( $integrity_results['validated_plugins'] as $valid_dir ) {
				$integrity_whitelist[] = str_replace( ABSPATH, '', $valid_dir['Path'] );
			}
		}

		$custom_paths = self::get_whitelist_paths_and_patterns();

		$merged_whitelist = array_merge(
			self::get_whitelist(),
			$custom_paths,
			$integrity_whitelist,
			array(
				ABSPATH . 'wp-admin/',
				trailingslashit( ABSPATH . WPINC ),
			)
		);
		$malscan->setIgnore( $merged_whitelist );

		$scanresults = $malscan->do_scan( ABSPATH );
		Wf_Sn_Ms_Util::clear_scan_progress();
		if ( $scanresults ) {
			self::update_results( $scanresults, 'do_mal_scan' );
			self::mark_scan_complete();
		}
		return $scanresults;
	}

	/**
	 * retreive results data
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $key    Default: null
	 * @return  array
	 */
	public static function get_results( $key = null ) {
		$cache = get_option( WF_SN_MS_RESULTS );

		if ( isset( $key ) && isset( $cache[ $key ] ) ) {
			return $cache[ $key ];
		} elseif ( isset( $key ) ) {
			return array();
		} else {
			return $cache;
		}
	}



	/**
	 * ajax_revert_whitelist_file.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, June 18th, 2024.
	 * @access  public static
	 * @global
	 * @return  void
	 */
	public static function ajax_revert_whitelist_file() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}
	}


	/**
	 * add file hash to whitelist
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @version v1.0.1  Tuesday, June 18th, 2024.
	 * @access  public static
	 * @return  void
	 */
	public static function add_whitelisted_file() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		$whitelist = get_option( WF_SN_MS_WHITELIST, array() );

		if ( isset( $_POST['hash'] ) && isset( $_POST['nonce'] ) && isset( $_POST['filename'] ) ) {
			$hash     = trim( $_POST['hash'] );
			$nonce    = trim( $_POST['nonce'] );
			$filename = sanitize_text_field( wp_unslash( $_POST['filename'] ) );

			// Validate the secure token
			if ( ! \WPSecurityNinja\Plugin\Wf_Sn_Crypto::validate_secure_file_token( $filename, $hash, $nonce, 'whitelist_file' ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'Invalid or expired security token.', 'security-ninja' ),
					)
				);
			}

			$new = array(
				'hash'     => esc_attr( $hash ),
				'filename' => esc_attr( $filename ),
			);

			// Check for duplicates
			$is_duplicate = false;
			foreach ( $whitelist as $item ) {
				if ( ! is_array( $item ) || ! isset( $item['hash'], $item['filename'] ) ) {
					continue;
				}
				if ( $item['hash'] === $new['hash'] && $item['filename'] === $new['filename'] ) {
					$is_duplicate = true;
					break;
				}
			}

			if ( ! $is_duplicate ) {
				$whitelist[] = $new;
				update_option( WF_SN_MS_WHITELIST, $whitelist, false );

				// Generate fresh output and get accurate count
				$fresh_output     = self::generate_fresh_output();
				$suspicious_count = self::count_suspicious_files();

				wp_send_json_success(
					array(
						'message'          => __( 'File added to whitelist successfully.', 'security-ninja' ),
						'fresh_output'     => $fresh_output,
						'suspicious_count' => $suspicious_count,
					)
				);
			} else {
				// Handle the duplicate case - does not really matter anyway, as long as the value is saved.
				wp_send_json_success(
					array(
						'message' => __( 'File was already in whitelist.', 'security-ninja' ),
					)
				);
			}
		}
	}


	/**
	 * revert_whitelisted_file.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Tuesday, June 18th, 2024.
	 * @access  public static
	 * @return  void
	 */
	public static function revert_whitelisted_file() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		if ( ! isset( $_POST['hash'], $_POST['nonce'], $_POST['filename'] ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Invalid input.', 'security-ninja' ),
				)
			);
		}

		$hash     = trim( wp_unslash( $_POST['hash'] ) );
		$nonce    = trim( wp_unslash( $_POST['nonce'] ) );
		$filename = sanitize_text_field( wp_unslash( $_POST['filename'] ) );

		// Validate the secure token (action-specific; do not use token hash to find DB rows).
		if ( ! \WPSecurityNinja\Plugin\Wf_Sn_Crypto::validate_secure_file_token( $filename, $hash, $nonce, 'revert_whitelist' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Invalid or expired security token.', 'security-ninja' ),
				)
			);
		}

		$whitelist = get_option( WF_SN_MS_WHITELIST, array() );
		if ( ! is_array( $whitelist ) ) {
			$whitelist = array();
		}

		$count_before = count( $whitelist );

		$whitelist = array_values(
			array_filter(
				$whitelist,
				function ( $item ) use ( $filename ) {
					if ( ! is_array( $item ) ) {
						return true;
					}
					// Pattern-only textarea entries have no per-file row to revert from this UI.
					if ( ! empty( $item['pattern'] ) && empty( $item['filename'] ) ) {
						return true;
					}
					if ( empty( $item['filename'] ) ) {
						return true;
					}
					return ! self::malware_scanner_whitelist_paths_match( $filename, $item['filename'] );
				}
			)
		);

		if ( count( $whitelist ) === $count_before ) {
			wp_send_json_error(
				array(
					'message' => __( 'File was not found in the whitelist.', 'security-ninja' ),
				)
			);
		}

		// Remove duplicates based on serialized rows.
		$whitelist = array_map( 'unserialize', array_unique( array_map( 'serialize', $whitelist ) ) );

		update_option( WF_SN_MS_WHITELIST, $whitelist, false );

		// Generate fresh output and get accurate count.
		$fresh_output     = self::generate_fresh_output();
		$suspicious_count = self::count_suspicious_files();

		wp_send_json_success(
			array(
				'message'          => __( 'File removed from whitelist successfully.', 'security-ninja' ),
				'fresh_output'     => $fresh_output,
				'suspicious_count' => $suspicious_count,
			)
		);
	}

	/**
	 * Whether two stored paths refer to the same file for whitelist revert (absolute vs relative under ABSPATH).
	 *
	 * @param string $path_a Request path.
	 * @param string $path_b Stored whitelist filename.
	 * @return bool
	 */
	private static function malware_scanner_whitelist_paths_match( $path_a, $path_b ) {
		$a = wp_normalize_path( (string) $path_a );
		$b = wp_normalize_path( (string) $path_b );
		if ( $a === $b ) {
			return true;
		}
		$base = wp_normalize_path( ABSPATH );
		$a_rel = ( strpos( $a, $base ) === 0 ) ? ltrim( substr( $a, strlen( $base ) ), '/' ) : $a;
		$b_rel = ( strpos( $b, $base ) === 0 ) ? ltrim( substr( $b, strlen( $base ) ), '/' ) : $b;
		return $a_rel === $b_rel;
	}






	/**
	 * remove all files from user created whitelist
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function reset_whitelist() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		update_option( WF_SN_MS_WHITELIST, array(), false );

		wp_send_json_success();
	}

	/**
	 * Save path patterns from the "Exclude paths from scan" form.
	 * Removes existing pattern entries and adds new ones from textarea.
	 *
	 * @return bool True if saved and redirecting, false otherwise.
	 */
	public static function save_whitelist_patterns() {
		if ( ! isset( $_POST['wf_sn_ms_save_patterns_nonce'] ) || ! isset( $_POST['action'] ) || $_POST['action'] !== 'wf_sn_ms_save_patterns' ) {
			return false;
		}
		if ( ! wp_verify_nonce( sanitize_key( $_POST['wf_sn_ms_save_patterns_nonce'] ), 'wf_sn_ms_save_patterns' ) ) {
			return false;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}
		$raw       = isset( $_POST['wf_sn_ms_whitelist_patterns'] ) ? wp_unslash( $_POST['wf_sn_ms_whitelist_patterns'] ) : '';
		$raw       = sanitize_textarea_field( $raw );
		$lines     = array_filter( array_map( 'trim', explode( "\n", $raw ) ) );
		$lines     = array_map( 'sanitize_text_field', $lines );
		$lines     = array_unique( array_filter( $lines ) );
		$whitelist = get_option( WF_SN_MS_WHITELIST, array() );
		if ( ! is_array( $whitelist ) ) {
			$whitelist = array();
		}
		$whitelist = array_filter(
			$whitelist,
			function ( $item ) {
				return ! isset( $item['pattern'] );
			}
		);
		$whitelist = array_values( $whitelist );
		foreach ( $lines as $line ) {
			$whitelist[] = array( 'pattern' => $line );
		}
		update_option( WF_SN_MS_WHITELIST, $whitelist, false );
		return true;
	}


	/**
	 * Reset scan results via AJAX (clears stored results and last-run state).
	 *
	 * @author  Security Ninja
	 * @since   v1.0.0
	 * @access  public static
	 * @return  void
	 */
	public static function reset_results() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		delete_option( WF_SN_MS_RESULTS );
		delete_option( WF_SN_MS_SCAN_TOTAL_TIME );
		delete_option( WF_SN_MS_INTEGRITY_RESULTS );
		delete_option( WF_SN_MS_CACHE );

		wp_send_json_success();
	}






	/**
	 * delete file via ajax
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function delete_file() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		$file = isset( $_POST['filename'] ) ? sanitize_text_field( wp_unslash( $_POST['filename'] ) ) : '';

		// Validate the filename
		if ( empty( $file ) || ! is_string( $file ) || strpos( $file, '..' ) !== false ) {
			wp_send_json_error(
				array(
					'message' => __( 'Invalid filename.', 'security-ninja' ),
				)
			);
		}

		// Validate that the file is within WordPress installation directory
		if ( ! \WPSecurityNinja\Plugin\FileViewer::is_within_wordpress_installation( $file ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Access denied: File is not within WordPress installation directory.', 'security-ninja' ),
				)
			);
		}

		// Validate the secure token
		$hash  = isset( $_POST['hash'] ) ? trim( $_POST['hash'] ) : '';
		$nonce = isset( $_POST['nonce'] ) ? trim( $_POST['nonce'] ) : '';

		if ( empty( $hash ) || empty( $nonce ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Missing security token.', 'security-ninja' ),
				)
			);
		}

		if ( ! \WPSecurityNinja\Plugin\Wf_Sn_Crypto::validate_secure_file_token( $file, $hash, $nonce, 'delete_file' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Invalid or expired security token.', 'security-ninja' ),
				)
			);
		}

		// Use WordPress filesystem for secure file deletion
		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		if ( $wp_filesystem->delete( $file, false ) ) {
			$deletelist   = get_option( WF_SN_MS_DELETELIST, array() );
			$deletelist[] = $hash;
			$deletelist   = array_unique( $deletelist );
			update_option( WF_SN_MS_DELETELIST, $deletelist, false );

			// Generate fresh output and get accurate count
			$fresh_output     = self::generate_fresh_output();
			$suspicious_count = self::count_suspicious_files();

			wp_send_json_success(
				array(
					'message'          => __( 'File deleted successfully.', 'security-ninja' ),
					'fresh_output'     => $fresh_output,
					'suspicious_count' => $suspicious_count,
				)
			);
		} else {
			wp_send_json_error(
				array(
					'message' => __( 'Failed to delete file.', 'security-ninja' ),
				)
			);
		}
	} // delete_file




	/**
	 * ajax function for running scan
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   boolean $opt    Default: false
	 * @return  void
	 */
	public static function run_scan( $opt = false ) {

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			check_ajax_referer( 'sn_ms' );

			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'Failed.', 'security-ninja' ),
					)
				);
			}
		}

		$out['err'] = '';
		if ( ! $opt ) {
			if ( isset( $_POST['what'] ) ) {

				$opt = trim( htmlspecialchars( $_POST['what'], ENT_QUOTES, 'UTF-8' ) );
			} else {
				$opt = ''; // Default or error handling.
			}
		}

		switch ( $opt ) {
				// First time - So lets start
			case 'clean_cache':
				\WPSecurityNinja\Plugin\Utils::timerstart( 'WF_SN_MS_SCAN' );
				self::update_cache( array() );
				self::mark_scan_in_progress();
				delete_option( WF_SN_MS_DELETELIST );
				break;

			case 'update_samples':
				// Make sure we have the files as base for scanning
				$updateSamples = self::get_or_update_samples();
				break;

			case 'get_db_rows':
				global $wpdb;
				$query      = $wpdb->prepare( 'SELECT COUNT(option_id) FROM ' . $wpdb->options . ' WHERE option_name NOT LIKE %s', '\_%' );
				$cnt        = $wpdb->get_var( $query );
				$out['cnt'] = $cnt;
				break;

			case 'do_integrity_scan':
				$integrity_results = wf_sn_plugins_integrity::runInternal();
				if ( $integrity_results ) {
					self::update_results( $integrity_results, 'do_integrity_scan' );
				}
				$out['msg'] = 'Scan completed';
				break;

			case 'do_db_scan':
				$db_scan_results = self::scan_db();
				if ( $db_scan_results ) {
					self::update_results( $db_scan_results, 'do_db_scan' );

					$out['cnt'] = count( $db_scan_results );
					$out['msg'] = sprintf(
						__( 'Database scan completed. Found %d suspicious entries.', 'security-ninja' ),
						$out['cnt']
					);
				} else {
					$out['cnt'] = 0;
					$out['msg'] = __( 'Database scan completed. No suspicious entries found.', 'security-ninja' );
				}
				break;

			case 'do_mal_scan':
				require_once __DIR__ . '/class-wf-sn-ms-util.php';
				require_once __DIR__ . '/class-malware-scanner-adapter.php';
				$malscan = new MalwareScannerAdapter();
				$malscan->setFlagHideOk( true );
				$malscan->setFlagComments( true );
				$malscan->setFlagHideWhitelist( true );
				$malscan->setFlagNoStop( false );
				$malscan->setFlagLineNumber( true );

				$integrity_whitelist = array();
				$integrity_results   = self::get_results( 'do_integrity_scan' );
				if ( $integrity_results ) {
					if ( is_array( $integrity_results['validated_plugins'] ) ) {
						foreach ( $integrity_results['validated_plugins'] as $key => $valid_dir ) {
							$integrity_whitelist[] = str_replace( ABSPATH, '', $valid_dir['Path'] );
						}
					}
				}

				$custom_paths = self::get_whitelist_paths_and_patterns();

				$merged_whitelist = array_merge(
					self::get_whitelist(),
					$custom_paths,
					$integrity_whitelist,
					array(
						ABSPATH . 'wp-admin/',
						trailingslashit( ABSPATH . WPINC ),
					)
				);

				$malscan->setIgnore( $merged_whitelist );

				$scanresults = $malscan->do_scan( ABSPATH ); // WF_SN_PLUGIN_DIR WP_PLUGIN_DIR WP_CONTENT_DIR // ABS_PATH
				Wf_Sn_Ms_Util::clear_scan_progress();
				if ( $scanresults ) {
					self::update_results( $scanresults, 'do_mal_scan' );
				}
				break;

			case 'scan_db':
				$results = self::scan_db();
				self::update_results( $results, 'db_options' );
				$out['cnt'] = count( $results );
				break;

			case 'get_results':
				self::mark_scan_complete();
				$out = '<div class="sncard">' . self::generate_fresh_output() . '</div>';
				// Gets and saves the total scan time as an option
				update_option( WF_SN_MS_SCAN_TOTAL_TIME, \WPSecurityNinja\Plugin\Utils::timerstop( 'WF_SN_MS_SCAN' ), false );
				break;

			default:
				wp_send_json_error( __( 'Invalid option.', 'security-ninja' ) );
		}

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			wp_send_json_success( $out );
		} else {
			return $out;
		}
	}



	/**
	 * in_ignored_file_extensions.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   boolean $file   Default: false
	 * @return  boolean
	 */
	public static function in_ignored_file_extensions( $file = false ) {
		if ( ! $file ) {
			return false;
		}

		$ignored_extensions = self::get_ignored_file_extensions();
		if ( ! $ignored_extensions ) {
			return false;
		}

		$ext = pathinfo( $file, PATHINFO_EXTENSION );
		if ( in_array( $ext, $ignored_extensions, true ) ) {
			return true;
		}
		return false;
	}



	/**
	 * check if file is in whitelist, user or global one
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $filename
	 * @param   mixed   $hash
	 * @return  boolean
	 */

	public static function in_whitelist( $filename, $hash = null ) {
		$whitelist  = get_option( WF_SN_MS_WHITELIST, array() );
		$deletelist = get_option( WF_SN_MS_DELETELIST, array() );

		// Check if the file ends with '.DS_Store'
		if ( basename( $filename ) === '.DS_Store' ) {
			return true;
		}

		// Tests if file is in whitelist by filename (per-file entries only)
		foreach ( $whitelist as $whitelist_item ) {
			if ( isset( $whitelist_item['filename'] ) && $whitelist_item['filename'] === $filename ) {
				return true;
			}
		}

		// Tests if file is in whitelist by hash and filename (per-file entries only)
		foreach ( $whitelist as $whitelist_item ) {
			if ( isset( $whitelist_item['filename'] ) && ( empty( $hash ) || ( isset( $whitelist_item['hash'] ) && $whitelist_item['hash'] === $hash ) ) && $whitelist_item['filename'] === $filename ) {
				return true;
			}
		}

		// Tests if in list of files to delete
		foreach ( $deletelist as $deletelist_item ) {
			if ( is_array( $deletelist_item ) && ( empty( $hash ) || $deletelist_item['hash'] === $hash ) ) {
				return true;
			}
		}

		// Tests against known folder names to ignore (legacy internal whitelist)
		foreach ( self::get_whitelist() as $pattern ) {
			if ( fnmatch( $pattern, strtolower( $filename ) ) ) {
				return true;
			}
		}

		// Tests against user path/pattern entries (per-file paths + pattern textarea)
		foreach ( self::get_whitelist_paths_and_patterns() as $pattern ) {
			if ( fnmatch( $pattern, strtolower( $filename ) ) ) {
				return true;
			}
		}

		// If none of the conditions match, the file is not whitelisted
		return false;
	}


	/**
	 * Returns a formatted line in malware results
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $title
	 * @param   mixed   $path
	 * @param   mixed   $hash
	 * @param   mixed   $comment        Default: null
	 * @param   mixed   $matchedline    Default: null
	 * @param   mixed   $line           Default: null
	 * @param   mixed   $pattern        Default: null
	 * @param   boolean $delete_btn     [description]
	 * @param   boolean $whitelist_btn  [description]
	 * @param   mixed   $files          Default: null
	 * @return  mixed
	 */
	public static function format_result_line(
		$title,
		$path,
		$hash,
		$comment = null,
		$matchedline = null,
		$line = null,
		$pattern = null,
		$delete_btn = false,
		$whitelist_btn = false,
		$files = null,
		$revert_whitelist_btn = false,
		$view_file = false
	) {
		$out  = '<div class="sn-malware-filebox">';
		$out .= '<div class="sn-malware-title">';
		$out .= '<div class="sn-malware-title-inner">';
		$out .= '<div class="malpath">' . esc_html( $title ) . '</div>';

		$out .= '<div class="malactions">';

		if ( $delete_btn ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'delete_file' );
			$out  .= '<button class="button button-small input-button sn_ms_delete" data-filename="' . esc_attr( $path ) . '" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '">' . esc_html__( 'Delete', 'security-ninja' ) . '</button>';
		}

		if ( $whitelist_btn ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'whitelist_file' );
			$out  .= '<button class="button button-small input-button gray sn_ms_whitelist" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '" data-filename="' . esc_attr( $path ) . '">' . esc_html__( 'Whitelist', 'security-ninja' ) . '</button>';
		}

		if ( $revert_whitelist_btn ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'revert_whitelist' );
			$out  .= '<button class="button button-small input-button gray sn_ms_revert_whitelist" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '" data-filename="' . esc_attr( $path ) . '">' . esc_html__( 'Revert Whitelist', 'security-ninja' ) . '</button>';
		}

		if ( $view_file && \WPSecurityNinja\Plugin\FileViewer::can_view_file( $path ) && file_exists( $path ) ) {
			$file_view_url = \WPSecurityNinja\Plugin\FileViewer::generate_file_view_url( $path, $line );

			$out .= '<button class="button button-small input-button gray sn_ms_view_file" data-href="' . esc_attr( $file_view_url ) . '">' . esc_html__( 'View File', 'security-ninja' ) . '</button>';
		}
		$out .= '</div><!-- malactions -->';
		$out .= '</div>';
		$out .= '</div>';

		$out .= '<div class="sn-malware-file-code closed">';
		/*
																			if ( isset( $matchedline ) ) {
																			$out .= '<div class="matchedline"><code>' . esc_html( $matchedline ) . '</code></div>';
																			}
																			*/
		/*
																			if ( isset( $line ) ) {
																			$out .= '<div class="linenumber">' . esc_html__( 'Line:', 'security-ninja' ) . ' ' . esc_html( $line ) . '</div>';
																			}
																			*/
		if ( isset( $pattern ) ) {
			$out .= '<div class="pattern">Matched pattern "' . esc_html( $pattern ) . '"</div>';
		}

		if ( is_array( $files ) ) {
			$out .= '<div class="files"><ul>';
			foreach ( $files as $file ) {
				$out .= '<li>' . esc_html( $file ) . '</li>';
			}
			$out .= '</ul></div>';
		}

		if ( $comment ) {
			$out .= '<div class="comment">' . esc_html( $comment ) . '</div>';
		}

		if ( $path ) {
			$out .= '<div class="path">' . esc_html__( 'Located here:', 'security-ninja' ) . ' <code>' . esc_html( $path ) . '</code></div>';
		}

		$out .= '</div>';
		$out .= '</div>';

		return $out;
	}





	/**
	 * Check if a file should be included in results (exists and not whitelisted)
	 *
	 * @param string $filepath The file path to check
	 * @param string $filehash The file hash (optional)
	 * @return bool Whether the file should be included
	 */
	public static function should_include_file( $filepath, $filehash = null ) {
		// Skip files that don't exist
		if ( ! file_exists( $filepath ) ) {
			return false;
		}

		// Get hash if not provided
		if ( $filehash === null ) {
			$filehash = @md5_file( $filepath );
		}

		// Skip whitelisted files
		return ! self::in_whitelist( $filepath, $filehash );
	}

	/**
	 * Collect suspicious files that can be whitelisted (same sources as scan output).
	 *
	 * @return array<int, array{filename: string, hash: string|false}>
	 */
	public static function collect_whitelistable_files() {
		$files = array();
		$seen  = array();

		$malscan = self::get_results( 'do_mal_scan' );
		if ( isset( $malscan['files'] ) && is_array( $malscan['files'] ) ) {
			foreach ( $malscan['files'] as $fi ) {
				if ( empty( $fi['path'] ) || ! self::should_include_file( $fi['path'] ) ) {
					continue;
				}
				$path = $fi['path'];
				if ( isset( $seen[ $path ] ) ) {
					continue;
				}
				$seen[ $path ]   = true;
				$files[] = array(
					'filename' => $path,
					'hash'     => @md5_file( $path ),
				);
			}
		}

		$integrityscan = self::get_results( 'do_integrity_scan' );

		if ( isset( $integrityscan['modified_files'] ) && is_array( $integrityscan['modified_files'] ) ) {
			foreach ( $integrityscan['modified_files'] as $is ) {
				if ( empty( $is['files'] ) || ! is_array( $is['files'] ) ) {
					continue;
				}
				$base = isset( $is['Path'] ) ? $is['Path'] : '';
				foreach ( $is['files'] as $fi ) {
					$path = $base . $fi;
					if ( ! self::should_include_file( $path ) || isset( $seen[ $path ] ) ) {
						continue;
					}
					$seen[ $path ]   = true;
					$files[] = array(
						'filename' => $path,
						'hash'     => @md5_file( $path ),
					);
				}
			}
		}

		if ( isset( $integrityscan['unknown_files'] ) && is_array( $integrityscan['unknown_files'] ) ) {
			foreach ( $integrityscan['unknown_files'] as $is ) {
				if ( empty( $is['files'] ) || ! is_array( $is['files'] ) ) {
					continue;
				}
				foreach ( $is['files'] as $fi ) {
					$path = $fi;
					if ( ! self::should_include_file( $path ) || isset( $seen[ $path ] ) ) {
						continue;
					}
					$seen[ $path ] = true;
					$files[]       = array(
						'filename' => $path,
						'hash'     => @md5_file( $path ),
					);
				}
			}
		}

		if ( isset( $integrityscan['suspicious_plugins'] ) && is_array( $integrityscan['suspicious_plugins'] ) ) {
			foreach ( $integrityscan['suspicious_plugins'] as $entry ) {
				if ( empty( $entry['files'] ) || ! is_array( $entry['files'] ) ) {
					continue;
				}
				foreach ( $entry['files'] as $fi ) {
					if ( ! self::should_include_file( $fi ) || isset( $seen[ $fi ] ) ) {
						continue;
					}
					$seen[ $fi ] = true;
					$files[]     = array(
						'filename' => $fi,
						'hash'     => @md5_file( $fi ),
					);
				}
			}
		}

		return $files;
	}

	/**
	 * Whitelist all currently flagged suspicious files (AJAX).
	 *
	 * @return void
	 */
	public static function whitelist_all_suspicious_files() {
		check_ajax_referer( 'sn_ms' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		$to_whitelist = self::collect_whitelistable_files();
		if ( empty( $to_whitelist ) ) {
			wp_send_json_success(
				array(
					'message'            => __( 'No files to whitelist.', 'security-ninja' ),
					'whitelisted_count'  => 0,
					'fresh_output'       => self::generate_fresh_output(),
					'suspicious_count'   => self::count_suspicious_files(),
				)
			);
		}

		$whitelist = get_option( WF_SN_MS_WHITELIST, array() );
		if ( ! is_array( $whitelist ) ) {
			$whitelist = array();
		}

		$added = 0;
		foreach ( $to_whitelist as $entry ) {
			$new = array(
				'hash'     => esc_attr( $entry['hash'] ),
				'filename' => esc_attr( $entry['filename'] ),
			);
			$is_duplicate = false;
			foreach ( $whitelist as $item ) {
				if ( isset( $item['hash'], $item['filename'] ) && $item['hash'] === $new['hash'] && $item['filename'] === $new['filename'] ) {
					$is_duplicate = true;
					break;
				}
			}
			if ( ! $is_duplicate ) {
				$whitelist[] = $new;
				++$added;
			}
		}

		if ( $added > 0 ) {
			update_option( WF_SN_MS_WHITELIST, $whitelist, false );
		}

		wp_send_json_success(
			array(
				/* translators: %d: number of files whitelisted */
				'message'           => sprintf( _n( '%d file added to the whitelist.', '%d files added to the whitelist.', $added, 'security-ninja' ), $added ),
				'whitelisted_count' => $added,
				'fresh_output'      => self::generate_fresh_output(),
				'suspicious_count'  => self::count_suspicious_files(),
			)
		);
	}

	/**
	 * Count the number of suspicious files across all scan types
	 *
	 * @return int Number of suspicious files
	 */
	public static function count_suspicious_files() {
		$suspicious_count = 0;
		$malscan          = self::get_results( 'do_mal_scan' );
		$integrityscan    = self::get_results( 'do_integrity_scan' );

		// Count malware scan results
		if ( isset( $malscan['files'] ) ) {
			foreach ( $malscan['files'] as $fi ) {
				if ( self::should_include_file( $fi['path'] ) ) {
					++$suspicious_count;
				}
			}
		}

		// Count integrity scan results - modified files
		if ( isset( $integrityscan['modified_files'] ) && is_array( $integrityscan['modified_files'] ) ) {
			foreach ( $integrityscan['modified_files'] as $is ) {
				$filelist = $is['files'];
				if ( is_array( $filelist ) ) {
					foreach ( $filelist as $fi ) {
						$filename = ( isset( $is['Path'] ) ? $is['Path'] : '' ) . $fi;
						if ( self::should_include_file( $filename ) ) {
							++$suspicious_count;
						}
					}
				}
			}
		}

		// Count integrity scan results - unknown files
		if ( isset( $integrityscan['unknown_files'] ) && is_array( $integrityscan['unknown_files'] ) ) {
			foreach ( $integrityscan['unknown_files'] as $is ) {
				$filelist = $is['files'];
				if ( is_array( $filelist ) ) {
					foreach ( $filelist as $fi ) {
						if ( self::should_include_file( $fi ) ) {
							++$suspicious_count;
						}
					}
				}
			}
		}

		// Count suspicious plugin/theme folder findings
		if ( isset( $integrityscan['suspicious_plugins'] ) && is_array( $integrityscan['suspicious_plugins'] ) ) {
			foreach ( $integrityscan['suspicious_plugins'] as $entry ) {
				if ( empty( $entry['files'] ) || ! is_array( $entry['files'] ) ) {
					continue;
				}
				foreach ( $entry['files'] as $fi ) {
					if ( self::should_include_file( $fi ) ) {
						++$suspicious_count;
					}
				}
			}
		}

		return $suspicious_count;
	}

	/**
	 * Build HTML for suspicious plugin/theme folder findings.
	 *
	 * @param array $suspicious_plugins Integrity suspicious_plugins result.
	 * @param int   $cnt                Running count of issues (passed by reference via return increment).
	 * @return string
	 */
	public static function format_suspicious_plugins_section( $suspicious_plugins, &$cnt ) {
		if ( empty( $suspicious_plugins ) || ! is_array( $suspicious_plugins ) ) {
			return '';
		}

		$labels = class_exists( __NAMESPACE__ . '\\Wf_Sn_Ms_Suspicious_Plugins' )
			? Wf_Sn_Ms_Suspicious_Plugins::get_signal_labels()
			: array();
		$rows   = array();

		foreach ( $suspicious_plugins as $entry ) {
			if ( empty( $entry['files'] ) || ! is_array( $entry['files'] ) ) {
				continue;
			}

			$name         = isset( $entry['Name'] ) ? $entry['Name'] : basename( untrailingslashit( $entry['Path'] ?? '' ) );
			$signal_texts = array();
			if ( ! empty( $entry['signals'] ) && is_array( $entry['signals'] ) ) {
				foreach ( $entry['signals'] as $signal ) {
					$signal_texts[] = isset( $labels[ $signal ] ) ? $labels[ $signal ] : $signal;
				}
			}
			$guidance = __( 'Review recommended (not confirmed malware).', 'security-ninja' );
			if ( ! empty( $signal_texts ) ) {
				$guidance .= ' ' . implode( '; ', $signal_texts );
			}

			$group_rows = array();
			foreach ( $entry['files'] as $fi ) {
				if ( ! file_exists( $fi ) ) {
					continue;
				}
				if ( ! self::should_include_file( $fi ) ) {
					continue;
				}
				$filehash    = @md5_file( $fi );
				$group_rows[] = self::build_ms_integrity_file_row( $fi, $filehash, 'warning', $guidance );
				++$cnt;
			}

			if ( ! empty( $group_rows ) ) {
				$rows[] = array(
					'type'  => 'group_header',
					'label' => $name,
				);
				$rows   = array_merge( $rows, $group_rows );
			}
		}

		if ( empty( $rows ) ) {
			return '';
		}

		return self::render_ms_section(
			__( 'Suspicious plugin / theme folders (review recommended)', 'security-ninja' ),
			$rows,
			__( 'These folders have unusual structure often seen in malware debris. This is not a confirmed infection; review and remove if unrecognised.', 'security-ninja' )
		);
	}

	/**
	 * Build scan context line for the Malware summary strip.
	 *
	 * @return string
	 */
	private static function build_ms_scan_context() {
		$scan_status = self::get_results( 'scan_status' );
		if ( ! is_string( $scan_status ) ) {
			$scan_status = '';
		}

		$last_run = self::get_results( 'last_run' );
		if ( ( ! $last_run || ! is_int( $last_run ) ) && in_array( $scan_status, array( 'failed', 'in_progress' ), true ) ) {
			$failed_at = self::get_results( 'failed_at' );
			if ( is_int( $failed_at ) && $failed_at > 0 ) {
				return sprintf(
					/* translators: %1$s: formatted date/time when the incomplete scan failed */
					__( 'Incomplete scan at %1$s', 'security-ninja' ),
					date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $failed_at )
				);
			}
			return '';
		}

		if ( ! $last_run || ! is_int( $last_run ) ) {
			return '';
		}

		$parts = array();
		$parts[] = sprintf(
			/* translators: %1$s: formatted date/time of last scan */
			__( 'Last scan at %1$s', 'security-ninja' ),
			date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_run )
		);

		$total_scan_time = get_option( WF_SN_MS_SCAN_TOTAL_TIME );
		if ( $total_scan_time ) {
			$spenttime = self::seconds2human( $total_scan_time );
			if ( $spenttime ) {
				$parts[] = sprintf(
					/* translators: %s: human-readable scan duration */
					__( 'Duration %s', 'security-ninja' ),
					$spenttime
				);
			}
		}

		$wf_sn_ms_results = get_option( WF_SN_MS_RESULTS );
		if ( isset( $wf_sn_ms_results['do_mal_scan'] ) ) {
			$parts[] = sprintf(
				/* translators: 1: number of files scanned, 2: number of directories scanned */
				__( 'Scanned %1$s files in %2$s directories', 'security-ninja' ),
				number_format_i18n( $wf_sn_ms_results['do_mal_scan']['files_scanned'] ),
				number_format_i18n( $wf_sn_ms_results['do_mal_scan']['directories_scanned'] )
			);
		}

		return implode( ' · ', $parts );
	}

	/**
	 * Build Malware summary strip HTML.
	 *
	 * @param int $issue_count Number of suspicious files.
	 * @return string
	 */
	private static function build_ms_summary_strip( $issue_count ) {
		$scan_status = self::get_results( 'scan_status' );
		if ( ! is_string( $scan_status ) ) {
			$scan_status = '';
		}

		$last_run = self::get_results( 'last_run' );
		$is_failed_or_incomplete = in_array( $scan_status, array( 'failed', 'in_progress' ), true );

		// Incomplete / failed scans must never look like a clean finished run.
		if ( $is_failed_or_incomplete ) {
			$scan_context = self::build_ms_scan_context();
			$banner_text  = ( 'failed' === $scan_status )
				? __( 'Last malware scan did not finish — do not treat the site as clean. Run the scan again.', 'security-ninja' )
				: __( 'A malware scan is incomplete — do not treat the site as clean. Run the scan again.', 'security-ninja' );

			$out = '<div class="sn-ms-summary sncard">';
			if ( $scan_context ) {
				$out .= '<p class="description sn-ms-scan-context" id="sn-ms-scan-details">' . esc_html( $scan_context ) . '</p>';
			}
			$out .= '<div class="sn-ms-banner sn-ms-banner-failed"><strong>' . esc_html( $banner_text ) . '</strong></div>';
			$out .= '</div>';

			return $out;
		}

		if ( ! $last_run || ! is_int( $last_run ) ) {
			return '';
		}

		$scan_context = self::build_ms_scan_context();

		if ( $issue_count > 0 ) {
			$banner_class = 'sn-ms-banner-issues';
			$banner_text  = sprintf(
				/* translators: %d: number of suspicious files */
				_n( '%d issue needs your attention', '%d issues need your attention', $issue_count, 'security-ninja' ),
				number_format_i18n( $issue_count )
			);
		} else {
			$banner_class = 'sn-ms-banner-clean';
			$banner_text  = __( 'No suspicious files found — your last scan looks good', 'security-ninja' );
		}

		$out = '<div class="sn-ms-summary sncard">';
		if ( $scan_context ) {
			$out .= '<p class="description sn-ms-scan-context" id="sn-ms-scan-details">' . esc_html( $scan_context ) . '</p>';
		}
		$out .= '<div class="sn-ms-banner ' . esc_attr( $banner_class ) . '"><strong>' . esc_html( $banner_text ) . '</strong></div>';
		if ( $issue_count > 0 ) {
			$out .= '<p class="sn-ms-bulk-actions"><button type="button" class="button button-secondary" id="sn-ms-whitelist-all">' . esc_html__( 'Whitelist all', 'security-ninja' ) . '</button></p>';
		}
		$out .= '</div>';

		return $out;
	}

	/**
	 * Normalize an absolute or relative path to a path relative to ABSPATH.
	 *
	 * @param string $path File path.
	 * @return string
	 */
	private static function normalize_ms_relative_path( $path ) {
		$path = str_replace( '\\', '/', (string) $path );
		$abs  = rtrim( str_replace( '\\', '/', ABSPATH ), '/' ) . '/';
		if ( 0 === strpos( $path, $abs ) ) {
			return ltrim( substr( $path, strlen( $abs ) ), '/' );
		}
		return ltrim( $path, '/' );
	}

	/**
	 * Render file size and modification time beneath a findings path.
	 *
	 * @param string $path Absolute or relative file path.
	 * @return string
	 */
	private static function get_ms_file_meta_html( $path ) {
		$relative = self::normalize_ms_relative_path( $path );
		$full     = ABSPATH . ltrim( $relative, '/' );
		if ( ! is_file( $full ) ) {
			return '';
		}

		$size = size_format( (int) filesize( $full ) );
		$time = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) filemtime( $full ) );

		return '<div class="sn-ms-file-meta">' . esc_html(
			sprintf(
				/* translators: 1: file size, 2: file modification date/time */
				__( '%1$s · Modified %2$s', 'security-ninja' ),
				$size,
				$time
			)
		) . '</div>';
	}

	/**
	 * Render a severity badge for Malware findings tables.
	 *
	 * @param string $severity Severity key.
	 * @return string
	 */
	private static function render_ms_severity_badge( $severity ) {
		$labels = array(
			'critical' => __( 'Critical', 'security-ninja' ),
			'warning'  => __( 'Warning', 'security-ninja' ),
			'notice'   => __( 'Notice', 'security-ninja' ),
		);
		$label  = isset( $labels[ $severity ] ) ? $labels[ $severity ] : ucfirst( (string) $severity );
		return '<span class="sn-ms-severity sn-ms-severity-' . esc_attr( $severity ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * Render action buttons for a Malware findings table row.
	 *
	 * @param string $path Absolute file path.
	 * @param string $hash File hash.
	 * @param array  $row  Row options.
	 * @return string
	 */
	private static function render_ms_row_actions( $path, $hash, $row ) {
		$out = '<span class="malactions">';

		if ( ! empty( $row['view'] ) && \WPSecurityNinja\Plugin\FileViewer::can_view_file( $path ) && file_exists( $path ) ) {
			$line          = isset( $row['line'] ) ? $row['line'] : null;
			$file_view_url = \WPSecurityNinja\Plugin\FileViewer::generate_file_view_url( $path, $line );
			$out          .= '<button type="button" class="button button-small input-button gray sn_ms_view_file" data-href="' . esc_attr( $file_view_url ) . '">' . esc_html__( 'View File', 'security-ninja' ) . '</button> ';
		}

		if ( ! empty( $row['whitelist'] ) ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'whitelist_file' );
			$out  .= '<button type="button" class="button button-small input-button gray sn_ms_whitelist" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '" data-filename="' . esc_attr( $path ) . '">' . esc_html__( 'Whitelist', 'security-ninja' ) . '</button> ';
		}

		if ( ! empty( $row['revert_whitelist'] ) ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'revert_whitelist' );
			$out  .= '<button type="button" class="button button-small input-button gray sn_ms_revert_whitelist" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '" data-filename="' . esc_attr( $path ) . '">' . esc_html__( 'Revert Whitelist', 'security-ninja' ) . '</button> ';
		}

		if ( ! empty( $row['delete'] ) ) {
			$token = \WPSecurityNinja\Plugin\Wf_Sn_Crypto::generate_secure_file_token( $path, 'delete_file' );
			$out  .= '<button type="button" class="button button-small input-button gray sn_ms_delete" data-filename="' . esc_attr( $path ) . '" data-hash="' . esc_attr( $token['hash'] ) . '" data-nonce="' . esc_attr( $token['nonce'] ) . '">' . esc_html__( 'Delete', 'security-ninja' ) . '</button>';
		}

		$out .= '</span>';
		return trim( $out ) ? $out : '';
	}

	/**
	 * Render Malware findings as a WordPress admin table.
	 *
	 * @param array $rows Table rows.
	 * @return string
	 */
	private static function render_ms_findings_table( $rows ) {
		if ( empty( $rows ) ) {
			return '';
		}

		$out  = '<table class="widefat striped sn-ms-findings-table"><thead><tr>';
		$out .= '<th scope="col">' . esc_html__( 'File', 'security-ninja' ) . '</th>';
		$out .= '<th scope="col">' . esc_html__( 'Severity', 'security-ninja' ) . '</th>';
		$out .= '<th scope="col">' . esc_html__( 'Guidance', 'security-ninja' ) . '</th>';
		$out .= '<th scope="col">' . esc_html__( 'Actions', 'security-ninja' ) . '</th>';
		$out .= '</tr></thead><tbody>';

		foreach ( $rows as $row ) {
			$type = isset( $row['type'] ) ? (string) $row['type'] : 'file';

			if ( 'group_header' === $type ) {
				$label = isset( $row['label'] ) ? (string) $row['label'] : '';
				$out  .= '<tr class="sn-ms-group-header"><td colspan="4"><strong>' . esc_html( $label ) . '</strong></td></tr>';
				continue;
			}

			$file     = isset( $row['file'] ) ? (string) $row['file'] : '';
			$path     = isset( $row['path'] ) ? (string) $row['path'] : ABSPATH . ltrim( $file, '/' );
			$hash     = isset( $row['hash'] ) ? (string) $row['hash'] : '';
			$severity = isset( $row['severity'] ) ? (string) $row['severity'] : 'warning';
			$guidance = isset( $row['guidance'] ) ? (string) $row['guidance'] : '';

			$out .= '<tr data-ms-file="' . esc_attr( $file ) . '">';
			$out .= '<td><code class="sn-ms-file-path">' . esc_html( $file ) . '</code>' . self::get_ms_file_meta_html( $path ) . '</td>';
			$out .= '<td>' . self::render_ms_severity_badge( $severity ) . '</td>';
			$out .= '<td class="sn-ms-guidance">' . esc_html( $guidance ) . '</td>';
			$out .= '<td class="sn-ms-actions">' . self::render_ms_row_actions( $path, $hash, $row ) . '</td>';
			$out .= '</tr>';
		}

		$out .= '</tbody></table>';
		return $out;
	}

	/**
	 * Wrap a findings table in a section heading.
	 *
	 * @param string $title       Section title.
	 * @param array  $rows        Table rows.
	 * @param string $description Optional description.
	 * @return string
	 */
	private static function render_ms_section( $title, $rows, $description = '' ) {
		if ( empty( $rows ) ) {
			return '';
		}

		$file_count = 0;
		foreach ( $rows as $row ) {
			if ( 'file' === ( isset( $row['type'] ) ? (string) $row['type'] : 'file' ) ) {
				++$file_count;
			}
		}

		$out  = '<div class="sn-ms-section">';
		$out .= '<h4>' . esc_html( $title ) . ' (' . esc_html( number_format_i18n( $file_count ) ) . ')</h4>';
		if ( '' !== $description ) {
			$out .= '<p class="description">' . esc_html( $description ) . '</p>';
		}
		$out .= self::render_ms_findings_table( $rows );
		$out .= '</div>';

		return $out;
	}

	/**
	 * Group relative file paths by common WordPress location.
	 *
	 * @param array $relative_paths Relative paths.
	 * @return array
	 */
	private static function group_ms_files_by_location( $relative_paths ) {
		$groups = array(
			'plugins'    => array(),
			'themes'     => array(),
			'uploads'    => array(),
			'wp_content' => array(),
			'core'       => array(),
			'other'      => array(),
		);

		foreach ( $relative_paths as $path ) {
			$path = (string) $path;
			if ( 0 === strpos( $path, 'wp-content/plugins/' ) ) {
				$groups['plugins'][] = $path;
			} elseif ( 0 === strpos( $path, 'wp-content/themes/' ) ) {
				$groups['themes'][] = $path;
			} elseif ( 0 === strpos( $path, 'wp-content/uploads/' ) ) {
				$groups['uploads'][] = $path;
			} elseif ( 0 === strpos( $path, 'wp-content/' ) ) {
				$groups['wp_content'][] = $path;
			} elseif ( 0 === strpos( $path, 'wp-admin/' ) || 0 === strpos( $path, 'wp-includes/' ) ) {
				$groups['core'][] = $path;
			} else {
				$groups['other'][] = $path;
			}
		}

		return $groups;
	}

	/**
	 * Insert location group headers into flat file rows.
	 *
	 * @param array $file_rows Rows keyed by relative path.
	 * @return array
	 */
	private static function build_ms_grouped_table_rows( $file_rows ) {
		if ( empty( $file_rows ) ) {
			return array();
		}

		$labels = array(
			'plugins'    => __( 'Plugins', 'security-ninja' ),
			'themes'     => __( 'Themes', 'security-ninja' ),
			'uploads'    => __( 'Uploads', 'security-ninja' ),
			'wp_content' => __( 'wp-content', 'security-ninja' ),
			'core'       => __( 'WordPress core', 'security-ninja' ),
			'other'      => __( 'Other locations', 'security-ninja' ),
		);

		$groups  = self::group_ms_files_by_location( array_keys( $file_rows ) );
		$out     = array();

		foreach ( $groups as $key => $paths ) {
			if ( empty( $paths ) ) {
				continue;
			}
			sort( $paths, SORT_STRING );
			$out[] = array(
				'type'  => 'group_header',
				'label' => $labels[ $key ],
			);
			foreach ( $paths as $path ) {
				$out[] = $file_rows[ $path ];
			}
		}

		return $out;
	}

	/**
	 * Build table rows for signature-based malware matches.
	 *
	 * @param array $files Malware scan file results.
	 * @return array
	 */
	private static function build_ms_malware_scan_rows( $files ) {
		$file_rows = array();

		if ( ! is_array( $files ) ) {
			return array();
		}

		foreach ( $files as $file ) {
			if ( ! is_array( $file ) || ! isset( $file['path'], $file['hash'] ) ) {
				continue;
			}
			if ( ! self::should_include_file( $file['path'] ) ) {
				continue;
			}
			if ( self::in_whitelist( $file['path'], $file['hash'] ) ) {
				continue;
			}

			$rel      = self::normalize_ms_relative_path( $file['path'] );
			$guidance = '';
			if ( ! empty( $file['pattern'] ) ) {
				$guidance = sprintf(
					/* translators: %s: matched malware pattern name */
					__( 'Matched pattern: %s', 'security-ninja' ),
					$file['pattern']
				);
			}
			if ( ! empty( $file['comment'] ) ) {
				$guidance = trim( $guidance . ' ' . $file['comment'] );
			}
			if ( '' === $guidance ) {
				$guidance = __( 'This file matched malware scanner signature rules. Review and delete if unintended.', 'security-ninja' );
			}

			$file_rows[ $rel ] = array(
				'type'      => 'file',
				'file'      => $rel,
				'path'      => $file['path'],
				'hash'      => $file['hash'],
				'severity'  => 'critical',
				'guidance'  => $guidance,
				'view'      => true,
				'whitelist' => true,
				'delete'    => true,
				'line'      => isset( $file['line'] ) ? $file['line'] : null,
			);
		}

		return self::build_ms_grouped_table_rows( $file_rows );
	}

	/**
	 * Build a single integrity finding row.
	 *
	 * @param string $absolute_path Absolute file path.
	 * @param string $hash          File hash.
	 * @param string $severity      Severity key.
	 * @param string $guidance      Guidance text.
	 * @return array
	 */
	private static function build_ms_integrity_file_row( $absolute_path, $hash, $severity, $guidance ) {
		$rel = self::normalize_ms_relative_path( $absolute_path );
		return array(
			'type'      => 'file',
			'file'      => $rel,
			'path'      => $absolute_path,
			'hash'      => $hash,
			'severity'  => $severity,
			'guidance'  => $guidance,
			'view'      => true,
			'whitelist' => true,
			'delete'    => true,
		);
	}

	/**
	 * Generate fresh output using cached scan data but with current tokens
	 *
	 * @return string Fresh HTML output with current tokens
	 */
	public static function generate_fresh_output() {
		$findings = '';
		$cnt      = 0;

		$malscan       = self::get_results( 'do_mal_scan' );
		$integrityscan = self::get_results( 'do_integrity_scan' );

		if ( isset( $integrityscan['modified_files'] ) && is_array( $integrityscan['modified_files'] ) ) {
			$modified_rows = array();

			foreach ( $integrityscan['modified_files'] as $is ) {
				$filelist = isset( $is['files'] ) ? $is['files'] : array();
				if ( ! is_array( $filelist ) || empty( $filelist ) ) {
					continue;
				}

				$base_path   = isset( $is['Path'] ) ? $is['Path'] : '';
				$group_label = ! empty( $is['Name'] ) ? $is['Name'] : __( 'Modified plugin files', 'security-ninja' );
				$group_rows  = array();

				foreach ( $filelist as $fi ) {
					$filename = $base_path . $fi;
					$filehash = @md5_file( $filename );

					if ( ! self::in_whitelist( $filename, $filehash ) ) {
						$group_rows[] = self::build_ms_integrity_file_row(
							$filename,
							$filehash,
							'critical',
							__( 'Plugin or theme file checksum does not match the official package. Review for unauthorized changes.', 'security-ninja' )
						);
						++$cnt;
					}
				}

				if ( ! empty( $group_rows ) ) {
					$modified_rows[] = array(
						'type'  => 'group_header',
						'label' => $group_label,
					);
					$modified_rows   = array_merge( $modified_rows, $group_rows );
				}
			}

			if ( ! empty( $modified_rows ) ) {
				$findings .= self::render_ms_section(
					__( 'Modified plugin files', 'security-ninja' ),
					$modified_rows,
					__( 'These files differ from the official plugin or theme package checksums.', 'security-ninja' )
				);
			}
		}

		if ( isset( $integrityscan['unknown_files'] ) && is_array( $integrityscan['unknown_files'] ) ) {
			$unknown_rows = array();

			foreach ( $integrityscan['unknown_files'] as $is ) {
				$filelist = isset( $is['files'] ) ? $is['files'] : array();
				if ( ! is_array( $filelist ) || empty( $filelist ) ) {
					continue;
				}

				$group_label = ! empty( $is['Name'] ) ? $is['Name'] : __( 'Unknown files', 'security-ninja' );
				$group_rows  = array();

				foreach ( $filelist as $fi ) {
					if ( ! file_exists( $fi ) ) {
						continue;
					}
					$filehash = @md5_file( $fi );
					if ( ! self::in_whitelist( $fi, $filehash ) ) {
						$group_rows[] = self::build_ms_integrity_file_row(
							$fi,
							$filehash,
							'warning',
							__( 'This file is not part of the official plugin or theme package. Verify it is intentional.', 'security-ninja' )
						);
						++$cnt;
					}
				}

				if ( ! empty( $group_rows ) ) {
					$unknown_rows[] = array(
						'type'  => 'group_header',
						'label' => $group_label,
					);
					$unknown_rows   = array_merge( $unknown_rows, $group_rows );
				}
			}

			if ( ! empty( $unknown_rows ) ) {
				$findings .= self::render_ms_section(
					__( 'Unknown files', 'security-ninja' ),
					$unknown_rows,
					__( 'Unexpected files inside a plugin or theme folder.', 'security-ninja' )
				);
			}
		}

		if ( isset( $integrityscan['suspicious_plugins'] ) && is_array( $integrityscan['suspicious_plugins'] ) ) {
			$suspicious_output = self::format_suspicious_plugins_section( $integrityscan['suspicious_plugins'], $cnt );
			if ( $suspicious_output !== '' ) {
				$findings .= $suspicious_output;
			}
		}

		if ( isset( $malscan['files'] ) && is_array( $malscan['files'] ) ) {
			$filtered_malscan_results = array();
			foreach ( $malscan['files'] as $fi ) {
				if ( self::should_include_file( $fi['path'] ) ) {
					$filtered_malscan_results[] = $fi;
				}
			}

			if ( ! empty( $filtered_malscan_results ) ) {
				$malware_rows = self::build_ms_malware_scan_rows( $filtered_malscan_results );
				$findings    .= self::render_ms_section(
					__( 'Signs of suspicious or malicious code', 'security-ninja' ),
					$malware_rows,
					__( 'Signature-based matches in file contents. Review each file before deleting.', 'security-ninja' )
				);
			}
		}

		$total_suspicious_count = self::count_suspicious_files();

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			do_action( 'security_ninja_malware_scanner_done_scanning', $total_suspicious_count );
		}

		$out = self::build_ms_summary_strip( $total_suspicious_count );

		if ( '' !== trim( $findings ) ) {
			$out .= '<div id="sn-ms-findings" class="sn-ms-findings-wrap">' . $findings . '</div>';
		}

		return $out;
	}

	/**
	 * format_results.
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $files
	 * @return  mixed
	 */
	public static function format_results( $files ) {
		$rows = self::build_ms_malware_scan_rows( $files );
		if ( empty( $rows ) ) {
			return '';
		}

		return self::render_ms_section(
			__( 'Signs of suspicious or malicious code', 'security-ninja' ),
			$rows,
			__( 'Signature-based matches in file contents. Review each file before deleting.', 'security-ninja' )
		);
	}







	/**
	 * prepare results for output
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $options
	 * @return  mixed
	 */
	public static function format_db_results( $options ) {
		global $wpdb;
		$out = '';

		if ( ! $options ) {
			return '';
		}

		$out .= '<div class="sn-malware-filebox">';
		$out .= '<div class="sn-malware-title">';
		$out .= '<div class="sn-malware-title-inner">';
		$out .= '<span>' . __( 'Database table', 'security-ninja' ) . ' ' . DB_NAME . '.' . $wpdb->options . '</span> <a href="#" class="toggle-slide"><div class="dashicons-before dashicons-arrow-up-alt2"></div></a><br>';
		$out .= '</div>';
		$out .= '</div>';
		$out .= '<div class="sn-malware-file-code closed">';
		foreach ( $options as $key => $result ) {
			$result = htmlspecialchars( $result );
			$result = str_replace( array( '@span@', '@/span@' ), array( '<span class="sn_ms_highlight">', '</span>' ), $result );
			$out   .= '<div class="one-line">';
			$out   .= '<span class="line-number">' . $key . '</span> <p>' . $result . '</p>';
			$out   .= '</div>';
		} // foreach line
		$out .= '</div>';
		$out .= '</div>';

		return $out;
	} // format_db_results





	/**
	 * generate regular expression for malware search
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   string  $filetype   Default: 'php'
	 * @return  mixed
	 */
	public static function line_match_pattern( $filetype = 'php' ) {
		// Match whole words only
		$php = '/[\n\r\s\,](assert|file_get_contents|curl_exec|popen|proc_open|unserialize|eval|base64_encode|base64_decode|create_function|exec|shell_exec|system|passthru|ob_get_contents|file|curl_init|readfile|fopen|fsockopen|pfsockopen|fclose|fread|file_put_contents)\s*?\(/';
		return $php;
	}






	/**
	 * get_ignored_file_extensions.
	 *
	 * @author  Unknown
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @version v1.0.1  Friday, November 17th, 2023.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_ignored_file_extensions() {
		$request_uri_array = apply_filters(
			'securityninja_ignored_file_extensions',
			array(
				'mo',
				'po',
				'md',
				'png',
				'gif',
				'md',
				'pot',
				'css',
				'js',
				'woff',
				'woff2',
				'xml',
				'eot',
				'ttf',
				'svg',
				'DS_Store',
			)
		);
		return $request_uri_array;
	}







	/**
	 * Returns an array of option_names to be skipped in database scan
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  mixed
	 */
	public static function get_whitelisted_option_names() {
		$oknames = array(
			'wf_sn_ms_results', // lk - internal storing scan results
			'fs_accounts', // lk freemius
			'fs_api_cache', // lk freemius
			'jetpack_protect_key', // lk
			'mwp_public_keys', // lk managewp
		);

		return $oknames;
	}








	/**
	 * scans all option and transient strings in db.options
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  mixed
	 */
	public static function scan_db() {
		global $wpdb;
		$out = array();

		$records = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(option_id) FROM $wpdb->options WHERE option_name NOT LIKE %s", '\_%' ) );

		$pages = round( $records / 100 ) + 1;
		$pages = max( 13, $pages );

		$whitelisted_option_names = self::get_whitelisted_option_names();

		for ( $i = 0; $i < $pages; $i++ ) {
			$query = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name  NOT LIKE %s LIMIT %d, 100", '\_%', $i * 100 ) );
			foreach ( $query as $row ) {
				if ( ! in_array( $row->option_name, $whitelisted_option_names, true ) ) {
					$result = self::scan_file_line( $row->option_value, 0, true );
					if ( $result ) {
						$out[ $row->option_name ] = $result[0];

					}
				}
			}
			unset( $query );
			wp_cache_flush();
		}

		return $out;
	}









	/**
	 * Scan one file line
	 *
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   string  $line
	 * @param   int     $num
	 * @param   boolean $only_first_line
	 * @return  array|false
	 */
	public static function scan_file_line( $line, $num, $only_first_line = false ) {
		$line = trim( (string) $line );

		if ( empty( $line ) ) {
			return false;
		}

		$results = array();
		$output  = array();

		// Matches usual patterns
		preg_match_all( self::line_match_pattern( 'php' ), $line, $matches );
		if ( ! empty( $matches[1] ) ) {
			$results = $matches[1];
		}

		// Check for base64 encoded strings
		preg_match_all( '/[\'\"\$\\ \/]*?([a-zA-Z0-9]{' . strlen( base64_encode( 'rainbow + unicorn = magic.' ) ) . ',})/', $line, $matches ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		if ( ! empty( $matches[1] ) ) {
			$results = array_merge( $results, $matches[1] );
		}

		// Looks for iframes
		preg_match_all( '/<\s*?(i?frame)/', $line, $matches );
		if ( ! empty( $matches[1] ) ) {
			$results = array_merge( $results, $matches[1] );
		}

		// MailPoet Vulnerability
		preg_match_all( '/explode\s?\(chr\s?\(\s?\(\d{3}\s?-\s?\d{3}\s?\)\s?\)\s?,/', $line, $matches );
		if ( ! empty( $matches[0] ) ) {
			$results = array_merge( $results, $matches[0] );
		}

		if ( ! empty( $results ) ) {
			$results = array_unique( $results );

			foreach ( $results as $tag ) {
				$cut_line = self::cut_line( $line, $tag );
				if ( $cut_line === null ) {
					continue; // Skip if cut_line returns null
				}
				$string   = str_replace( $tag, '<span>' . esc_html( $tag ) . '</span>', $cut_line );
				$output[] = $string;
				if ( $only_first_line ) {
					break;
				}
			} // foreach result

			return $output;
		} // if results

		return false;
	}









	/**
	 * Trim code line
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   mixed   $line
	 * @param   mixed   $tag
	 * @param   integer $max    [description]
	 * @return  mixed
	 */
	public static function cut_line( $line, $tag, $max = 200 ) {
		if ( ! $line || ! $tag ) {
			return false;
		}

		if ( strlen( $tag ) > $max ) {
			return $tag;
		}

		$left = round( ( $max - strlen( $tag ) ) / 2 );
		$tag  = preg_quote( $tag );

		$output = preg_replace( '/(' . $tag . ')(.{' . $left . '}).{0,}$/', '$1$2 ...', $line );
		$output = preg_replace( '/^.{0,}(.{' . $left . ',})(' . $tag . ')/', '... $1$2', $output );

		return $output;
	} // cut_line







	/**
	 * Based on anon submission https://snippetsofcode.wordpress.com/2012/08/25/php-function-to-convert-seconds-into-human-readable-format-months-days-hours-minutes/
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @param   boolean $ss Default: false
	 * @return  mixed
	 */
	public static function seconds2human( $ss = false ) {
		if ( ! $ss ) {
			return false;
		}
		$s      = (int) $ss % 60;
		$minraw = floor( ( (int) $ss % 3600 ) / 60 );
		$m      = '';
		if ( $minraw > 0 ) {
			$m = sprintf( _n( '%s minute', '%s minutes', $minraw, 'security-ninja' ), esc_html( number_format_i18n( $minraw ) ) );
		}
		$hourraw = floor( ( (int) $ss % 86400 ) / 3600 );
		$h       = '';
		if ( $hourraw > 0 ) {
			$h = sprintf( _n( '%s hour', '%s hours', $hourraw, 'security-ninja' ), number_format_i18n( $hourraw ) );
		}

		return ltrim( "$h $m $s seconds" );
	}








	/**
	 * Display results
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function malware_page() {

		$last_run = self::get_results( 'last_run' );

		// Save path patterns form (redirect to avoid re-POST).
		if ( isset( $_POST['action'] ) && $_POST['action'] === 'wf_sn_ms_save_patterns' && self::save_whitelist_patterns() ) {
			wp_safe_redirect( add_query_arg( 'ms_patterns_saved', '1', wp_get_referer() ? wp_get_referer() : '' ) );
			exit;
		}

		if ( isset( $_GET['ms_patterns_saved'] ) && $_GET['ms_patterns_saved'] === '1' ) {
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Path patterns saved.', 'security-ninja' ) . '</p></div>';
		}

		echo '<div class="sncard">';

		echo '<h2>' . __( 'Malware Scanner', 'security-ninja' ) . '</h2>';

		self::get_or_update_samples();

		$pattern_last_update = get_option( WF_SN_MS_LAST_PATTERN_UPDATE, false );

		if ( version_compare( PHP_VERSION, '7.0', '<' ) ) {
			?>
			<p><strong>
					<?php
					printf( esc_html__( 'Warning - You are running PHP v. %1$s - Malware Scanner needs PHP 7+ ', 'security-ninja' ), PHP_VERSION );
					?>
				</strong></p>
			<?php
			$infolink = Utils::generate_sn_web_link( 'malware_docs', '/docs/installation-and-usage/why-upgrade-to-php-7/' );
			echo '<p>' . __( 'Why is this important?', 'security-ninja' ) . '</p><a href="' . esc_url( $infolink ) . '" target="_blank">' . __( 'Read more about PHP 7', 'security-ninja' ) . '</a> (opens a new window)</small></p>';

		} else {
			?>
			<button type="button" id="sn_ms_run_scan" class="button button-primary button-hero button-center"><?php esc_html_e( 'Scan your website', 'security-ninja' ); ?></button>
			<?php
			if ( isset( $last_run ) && is_int( $last_run ) ) {
				echo '<p class="sn-ms-reset-wrap"><a href="#" id="sn-ms-reset-results">' . esc_html__( 'Reset results', 'security-ninja' ) . '</a></p>';
			} else {
				$scan_status = self::get_results( 'scan_status' );
				if ( is_string( $scan_status ) && in_array( $scan_status, array( 'failed', 'in_progress' ), true ) ) {
					echo '<p class="sn-ms-reset-wrap"><a href="#" id="sn-ms-reset-results">' . esc_html__( 'Reset results', 'security-ninja' ) . '</a></p>';
				}
			}
		}
		if ( ! ( isset( $last_run ) && is_int( $last_run ) ) ) {
			echo '<p>' . __( 'Depending on the size of your website this can take several minutes.', 'security-ninja' ) . '</p>';
		}
		?>
		<div id="sn_ms_timer" style="display: none;"><span id="mscounterminutes">00</span>:<span id="mscounterseconds">00</span></div>

		<div id="sn_ms_results" class="testresults">

		<?php
		if ( $pattern_last_update ) {
			?>
						<p class="sn-infobox">
							<?php
							printf(
								// translators: Shows how many vulnerabilities are known and when list was updated
								esc_html__( 'Malware patterns list last updated %1$s (%2$s)', 'security-ninja' ),
								esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $pattern_last_update ) ),
								esc_html( human_time_diff( $pattern_last_update, time() ) . ' ' . __( 'ago', 'security-ninja' ) )
							);
							?>
						</p>
					<?php
		}
		?>
		<?php

					// Generate fresh output instead of using cached HTML
		// This ensures tokens are always current
		$scanresults = self::generate_fresh_output();

		if ( $scanresults ) {
			echo '<div id="sn_ms_scan_findings" class="sn-ms-scan-findings">';
			echo wp_kses_post( $scanresults );
			echo '</div>';

				$whitelist       = get_option( WF_SN_MS_WHITELIST, array() );
				$whitelist_files = array();
			if ( is_array( $whitelist ) ) {
				foreach ( $whitelist as $item ) {
					if ( isset( $item['filename'] ) && $item['filename'] !== '' ) {
						$whitelist_files[] = $item;
					}
				}
			}

			if ( $whitelist_files ) {

				?>
					<div class="whitelistcont card">
						<h3>
						<?php
						$count = count( $whitelist_files );
						printf( esc_html( _n( '%d whitelisted file', '%d whitelisted files', intval( $count ), 'security-ninja' ) ), $count );
						?>
						</h3>
						<ul class="whitelistedfiles">
						<?php

						foreach ( $whitelist_files as $wf ) {

							$parts            = explode( '/', $wf['filename'] );
							$last_part        = array_pop( $parts );
							$second_last_part = array_pop( $parts );
							$third_last_part  = array_pop( $parts );
							$fourth_last_part = array_pop( $parts );

							if ( $fourth_last_part ) {
								$filename_title = '.../' . $fourth_last_part . '/' . $third_last_part . '/' . $second_last_part . '/' . $last_part;
							} elseif ( $third_last_part ) {
								$filename_title = '.../' . $third_last_part . '/' . $second_last_part . '/' . $last_part;
							} elseif ( $second_last_part ) {
								$filename_title = '.../' . $second_last_part . '/' . $last_part;
							} else {
								$filename_title = '/' . $last_part;
							}

							echo self::format_result_line(
								$filename_title,
								$wf['filename'],
								$wf['hash'],
								'',
								null,
								null,
								false,
								true,
								false,
								$wf['filename'],
								true,
								true
							);
						}
						?>
						</ul>
						<p>
						<?php
						_e( 'These files are ignored from scans.', 'security-ninja' );
						?>
						</p>
						<?php
						echo '<a href="#" id="sn-ms-reset-whitelist" class="button button-secondary">' . __( 'Reset whitelisted files', 'security-ninja' ) . '</a>';
						?>
					</div>
				<?php
			}

			?>
				<div id="sn_ms_results_detailed">
					<div class="card">
						<?php
						$last_results = self::get_results();

						if ( isset( $last_results['do_integrity_scan']['validated_plugins'] ) ) {
							$validated = $last_results['do_integrity_scan']['validated_plugins'];

							if ( is_array( $validated ) ) {
								?>
								<h3>Plugins validated</h3>
								<p>These plugins were compared against the info in the public repository. There were no modifications detected.</p>
								<ul class="known-public-plugins">
									<?php
									foreach ( $validated as $key => $plugin ) {
										echo '<li><strong>' . esc_html( $plugin['Name'] ) . '</strong> by ' . htmlspecialchars( $plugin['Author'] ) . ' (v. ' . esc_html( $plugin['Version'] ) . ')</li>';
									}
									?>
								</ul>
								<?php
							}
						}
						?>
					</div>

					<?php
					if ( isset( $last_results['do_integrity_scan']['checksums_retrieval_failed'] ) ) {
						$failed = $last_results['do_integrity_scan']['checksums_retrieval_failed'];
						if ( is_array( $failed ) && ( count( $failed ) > 0 ) ) {
							?>
							<div class="card">
								<h3><?php esc_html_e( 'Plugins not in wordpress.org repository', 'security-ninja' ); ?></h3>
								<p><?php esc_html_e( 'WordPress.org checksums were not available for these plugins. They may be premium, custom, or privately distributed.', 'security-ninja' ); ?></p>
								<p><?php esc_html_e( 'Structural heuristics and the malware content scan still run on these paths. Findings appear above if review is recommended.', 'security-ninja' ); ?></p>
								<ul class="unknown-public-plugins">
									<?php
									// Filters out plugin names we do not want to show up.
									$ignore_arr = array( 'Security Ninja Pro', 'Security Ninja (Premium)' );
									foreach ( $failed as $key => $plugin ) {
										if ( ! in_array( $plugin['Name'], $ignore_arr, true ) ) {
											?>
											<li><a href="<?php echo esc_url( $plugin['PluginURI'] ); ?>" target="_blank"><?php echo esc_html( $plugin['Name'] ); ?></a> by <?php echo esc_html( $plugin['Author'] ); ?> v. <?php echo esc_html( $plugin['Version'] ); ?></li>
											<?php
										}
									}
									?>
								</ul>

							</div>
							<?php
						}
					}
					?>

				</div><!-- .details_box -->
				<!--		</div>--><!-- #sn_ms_results_detailed -->
			<?php
		} else {
			?>

				<p><?php esc_html_e( 'Click the button to run the first test.', 'security-ninja' ); ?></p>

			<?php
		}
		?>
		</div>
		</div>

		<div class="sncard" id="sn_ms_cont">
		<?php
		$whitelist_option = get_option( WF_SN_MS_WHITELIST, array() );
		$pattern_lines    = array();
		if ( is_array( $whitelist_option ) ) {
			foreach ( $whitelist_option as $item ) {
				if ( isset( $item['pattern'] ) && $item['pattern'] !== '' ) {
					$pattern_lines[] = $item['pattern'];
				}
			}
		}
		$doc_link = Utils::generate_sn_web_link( 'malware_docs', '/docs/malware-scanner/how-to-exclude-paths/' );
		?>
		<div class="section sncard settings-card">
			<form method="post" action="">
				<?php wp_nonce_field( 'wf_sn_ms_save_patterns', 'wf_sn_ms_save_patterns_nonce' ); ?>
				<input type="hidden" name="action" value="wf_sn_ms_save_patterns" />
				<h2 class="ss_header"><span class="dashicons dashicons-admin-generic"></span> <?php esc_html_e( 'Exclude paths from scan', 'security-ninja' ); ?></h2>
				<table class="form-table">
					<tbody>
						<tr>
							<th scope="row">
								<label for="wf_sn_ms_whitelist_patterns">
									<p class="description"><?php esc_html_e( 'Paths or patterns listed here are excluded from malware scans. One per line. Use * as wildcard (e.g. */plugins/plugin-name/*).', 'security-ninja' ); ?></p>
								</label>
							</th>
							<td></td>
						</tr>
						<tr>
							<td class="fullwidth" colspan="2">
								<textarea name="wf_sn_ms_whitelist_patterns" id="wf_sn_ms_whitelist_patterns" rows="5" class="large-text code" placeholder="*/plugins/plugin-name/*"><?php echo esc_textarea( implode( "\n", $pattern_lines ) ); ?></textarea>
								<p class="description"><?php esc_html_e( 'Example: */plugins/plugin-name/* or *wp-content/uploads/cache/*', 'security-ninja' ); ?></p>
								<p>
									<?php
									echo wp_kses(
										sprintf(
											/* translators: %s: link to documentation */
											__( 'You can exclude whole plugin or theme folders from scans. <a href="%s" target="_blank" rel="noopener noreferrer">Learn how</a>', 'security-ninja' ),
											esc_url( $doc_link )
										),
										array(
											'a' => array(
												'href'   => true,
												'target' => true,
												'rel'    => true,
											),
										)
									);
									?>
								</p>
							</td>
						</tr>
						<tr>
							<td colspan="2">
								<p class="submit"><input type="submit" value="<?php esc_attr_e( 'Save patterns', 'security-ninja' ); ?>" class="button button-secondary" name="wf_sn_ms_save_patterns_submit" /></p>
							</td>
						</tr>
					</tbody>
				</table>
			</form>
		</div>
		</div>
		<?php
	}








	/**
	 * Display warning if test were never run
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function warnings() {

		if ( ! wf_sn::is_plugin_page() ) {
			return;
		}

		$memory = ini_get( 'memory_limit' );
		if ( wf_sn::is_plugin_page() && -1 !== (int) $memory && (int) $memory < 64 && false === stripos( $memory, 'G' ) ) {
			echo '<div class="secnin-notice notice notice-warning updated sn_ms_warning"><p>Your PHP memory limit is only ' . esc_html( $memory ) . 'MB. Malware Scanner may not be able to scan all your files. Please ask your host to increase the limit.</p></div>';
		}

		/*
																					$last_run = self::get_results('last_run');
																					if (!empty($last_run) && wf_sn::is_plugin_page() && (time() - 30 * 24 * 60 * 60) > $last_run) {
																					echo '<div class="secnin-notice notice notice-warning wf-sn-ms-not-run is-dismissible"><p>';
																					echo sprintf(esc_html__("Malware Scanner tests were not run for more than %s days. It's advisable to run them once in a while. Click 'Scan files' to run them now and check your files for malware.", 'security-ninja'), '30');
																					echo '</p></div>';
																					}
																					*/
	}






	public static function activate() {
		// Placeholder, needed for premium version - when running "Update database".
	}





	/**
	 * Clean-up when deactivated
	 *
	 * @author  Unknown
	 * @since   v0.0.1
	 * @version v1.0.0  Thursday, April 28th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function deactivate() {
		$centraloptions = Wf_Sn::get_options();
		if ( ! isset( $centraloptions['remove_settings_deactivate'] ) ) {
			return;
		}
		if ( $centraloptions['remove_settings_deactivate'] ) {
			delete_option( 'wf_sn_ms_options' );
			delete_option( WF_SN_MS_RESULTS );
			delete_option( WF_SN_MS_INTEGRITY_RESULTS );
			delete_option( WF_SN_MS_CACHE );
			delete_option( WF_SN_MS_WHITELIST );
			delete_option( WF_SN_MS_DELETELIST );
			delete_option( WF_SN_MS_SCAN_TOTAL_TIME );
			delete_option( WF_SN_MS_SK );
			delete_option( WF_SN_MS_IV );
			delete_option( WF_SN_MS_LAST_PATTERN_UPDATE );
		}
		// Removes scheduled event to download samples
		$timestamp = wp_next_scheduled( 'secnin_update_samples' );

		if ( $timestamp ) {
			wp_unschedule_event( $timestamp, 'secnin_update_samples' );
		}

		// remove the upload folder with cached files.
		$upload_dir = wp_upload_dir();
		$json_dir   = $upload_dir['basedir'] . '/security-ninja/';

		if ( file_exists( $json_dir ) ) {
			$it    = new \RecursiveDirectoryIterator( $json_dir, \RecursiveDirectoryIterator::SKIP_DOTS );
			$files = new \RecursiveIteratorIterator( $it, \RecursiveIteratorIterator::CHILD_FIRST );
			foreach ( $files as $file ) {
				if ( $file->isDir() ) {
					rmdir( $file->getRealPath() );
				} else {
					unlink( $file->getRealPath() );
				}
			}
			rmdir( $json_dir );
		}
	}
}

add_action( 'plugins_loaded', array( __NAMESPACE__ . '\wf_sn_ms', 'init' ) );
register_deactivation_hook( WF_SN_BASE_FILE, array( __NAMESPACE__ . '\wf_sn_ms', 'deactivate' ) );
