<?php
/**
 *       @file  qtrAjaxHandler.php
 *      @brief  This module contains AJAX callbacks
 *
 *     @author  Quttera (qtr), contactus@quttera.com
 *
 *   @internal
 *     Created  01/17/2016
 *     Company  Quttera
 *   Copyright  Copyright (c) 2016, Quttera
 *
 * This source code is released for free distribution under the terms of the
 * GNU General Public License as published by the Free Software Foundation.
 * =====================================================================================
 */


require_once('qtrOptions.php');
require_once('qtrExternalScanner.php');
require_once('qtrUtils.php');
require_once('qtrLogger.php');
require_once('qtrFilesScanner.php');
require_once('qtrStats.php');
require_once('qtrScanLock.php');
require_once('qtrIgnoreList.php');
require_once('qtrThreatsWhiteList.php');
require_once('qtrFilesWhiteList.php');
require_once('qtrFsSnapShot.php');
require_once('qtrSettings.php');
require_once('qtrAlarmsManager.php');
require_once('qtrBruteForce.php');
require_once('qtrExternalScanReport.php');
require_once('qtrIPManager.php');


define( 'QTR_SCAN_CRON_ARGS', 'qtr_scan_cron_args');

define( 'QTR_ESCAN_STATUS', 'qtr_escan_status');

define( 'QTR_SCAN_CRON_PERIOD', 5*60 );

define( 'QTR_SCAN_CRON_TIMEOUT',  QTR_SCAN_CRON_PERIOD - 5 );

/*
 * add filter to add 30 seconds cron period
 */
add_filter( 'cron_schedules', 'qtr_scanner_custom_cron_schedule' );

/*
 * maps cron hook to appropriate callback to be invoked for internal scan
 */
add_action( 'qtr_internal_scan_cron_hook', 'on_qtr_scanner_internal_scan_cron_event' );

add_action( 'qtr_heur_internal_scan_cron_hook', 'on_qtr_scanner_heur_internal_scan_cron_event' );


/**
 * Adds a custom cron schedule for every 5 minutes.
 *
 * @param array $schedules An array of non-default cron schedules.
 * @return array Filtered array of non-default cron schedules.
 */
function qtr_scanner_custom_cron_schedule( $schedules ) {
    $schedules[ 'qtrScanPeriod' ] = array( 'interval' => QTR_SCAN_CRON_PERIOD, 'display' => __( sprintf("Every %d seconds",QTR_SCAN_CRON_PERIOD)));
    return $schedules;
}


/**
 * @brief   removes all instances of registered cron job
 * @return  nothing 
 */
function clean_internal_scan_hook()
{
    wp_cache_flush();
    do {
        $timestamp = wp_next_scheduled( 'qtr_internal_scan_cron_hook' );
        wp_unschedule_event( $timestamp, 'qtr_internal_scan_cron_hook' );
    }while(($timestamp = wp_next_scheduled( 'qtr_internal_scan_cron_hook' )));
    wp_cache_flush(); 
}

/**
 * @brief   removes all instances of registered cron job
 * @return  nothing 
 */
function clean_heur_internal_scan_hook()
{
    wp_cache_flush();
    do {
        $timestamp = wp_next_scheduled( 'qtr_heur_internal_scan_cron_hook' );
        wp_unschedule_event( $timestamp, 'qtr_heur_internal_scan_cron_hook' );
    }while(($timestamp = wp_next_scheduled( 'qtr_heur_internal_scan_cron_hook' )));
    wp_cache_flush(); 
}


function schedule_internal_scan_hook()
{
    /*
     * submit cron job event to run internal scan
     */
    wp_schedule_event( time() + 10, 'qtrScanPeriod', 'qtr_internal_scan_cron_hook');

    $logger = new CQtrLogger();
    $logger->Info(
        sprintf("Internal scan scheduled. Next run %s", 
            gmdate("H:i:s", 
                wp_next_scheduled('qtr_internal_scan_cron_hook'))));
}


function schedule_heur_internal_scan_hook()
{
    /*
     * submit cron job event to run internal scan
     */
    wp_schedule_event( time() + 10, 'qtrScanPeriod', 'qtr_heur_internal_scan_cron_hook');

    $logger = new CQtrLogger();
    $logger->Info(
        sprintf("High sensitive internal scan scheduled. Next run %s", 
            gmdate("H:i:s", 
                wp_next_scheduled('qtr_heur_internal_scan_cron_hook'))));
}


/**
 * @brief       stores provided file system snapshot (list of files to scan)
 * @param[in]   $snapshot - snapshot object to store
 * @return      FALSE on failure and TRUE on success
 */
function store_snapshot($snapshot, $logger )
{
    $json = $snapshot->ToString();
    $rc = FALSE;
    $deprecated = null;
    $autoload = 'no';

    if( !$json && $snapshot->FilesCount() > 0){
        $logger->Error("Failed to serialize filesystem snapshot");
        return FALSE;
    }

    if( defined("QTR_FS_SNAPSHOT") and QTR_FS_SNAPSHOT ){
        $logger->Info("Storing file based snapshot");
        $rc = CQtrOptions::SaveOption( QTR_SCAN_CRON_ARGS, $json, $deprecated, $autoload, $logger);
        if(!$rc){ $logger->Error("Failed to store filesystem snapshot"); }
        else{ $logger->Info("Filesystem snapshot stored successfully"); }
    }else if ( CQtrOptions::GetOption( QTR_SCAN_CRON_ARGS,$logger) !== false ) {
        $rc = CQtrOptions::UpdateOption( QTR_SCAN_CRON_ARGS, $json, $logger );
    }else{
        $deprecated = null;
        $autoload = 'no';
        $rc = CQtrOptions::AddOption( QTR_SCAN_CRON_ARGS, $json, $deprecated, $autoload, $logger);
    }

    return $rc;
}


/**
 * @brief   loads previously stored 
 * @return  on error returns NULL, on success returns loaded and initialized snapshot object 
 */
function load_snapshot($logger)
{
    $json = NULL;
    if( defined("QTR_FS_SNAPSHOT") and QTR_FS_SNAPSHOT ){
        $logger->Info("Loading file based snapshot");
        $json = CQtrOptions::LoadOption( QTR_SCAN_CRON_ARGS, false, $logger);
        /*
        $logger->Info(sprintf("Loaded json [%s]",$json));
        */
    }else{
        $json = CQtrOptions::GetOption( QTR_SCAN_CRON_ARGS, $logger);
    }

    if( !$json ){
        return NULL;
    }

    $fs = new CQtrFsSnapShot();
    $fs->FromString($json);
    return $fs;
}


function force_next_cron_job(){
    spawn_cron();
}

/**
 * @brief       WordPress cron job callback for normal internal file scanning
 * @details     Processes the filesystem snapshot in time-limited batches to avoid system overload.
 *              Uses settings to determine scan mode (normal or heuristic).
 *              Respects QTR_SCAN_CRON_TIMEOUT to prevent execution timeout.
 * @param[in]   $args - (optional) list of input arguments from WordPress cron
 * @return      void - Exits execution when scan completes or times out
 * @see         QutteraSettings::IsHeuristicMode() for determining scan type
 * @see         CQtrFilesScanner for file scanning implementation
 */
function on_qtr_scanner_internal_scan_cron_event($args=NULL)
{
    @set_time_limit(0);

    $logger = new CQtrLogger();
    $snapshot = load_snapshot($logger);
    if( !$snapshot ){
        $logger->Error("Failed to locate filesystem snapshot");
        return;
    }
    
    // Load settings
    $settings = QutteraSettings::LoadFromOptions();
    if (!$settings) {
        $settings = new QutteraSettings();
    }

    $scanner = "heuristic";
    if (!$settings->IsHeuristicMode()) {
        $scanner = "normal";
    }

    $logger->Info(sprintf("Snapshot %d elements, scanner %s", $snapshot->FilesCount(), $scanner));

    $stime = time();
    $etime = $stime + QTR_SCAN_CRON_TIMEOUT;
    // Instantiate file scanner with scan mode from settings (true for heuristic, false for normal)
    $scanner = new CQtrFilesScanner($settings->IsHeuristicMode());
    $scanner->Initialize();

    while(time() < $etime){
        /*
         * runing limited period of time to prevent system overload
         */
        $item = $snapshot->Pop();

        if($item == NULL ){
            /*
             * snapshot is empty, all scanned
             */
            $scanner->Finalize();
            /*
             * remove callback registration
             */
            clean_internal_scan_hook();
            $logger->Info(sprintf("Investigation of %s done", ABSPATH));
            return;
        }

        /*
         * In case scan of next file will take too much and this session will be killed
         */
        store_snapshot($snapshot, $logger);

	    if(is_file($item)){
			$scanner->ScanFile($item);
		}else if($scanner->IsIgnored($item)){
			$logger->Info(sprintf("Skipping ignored %s"));
	    }else{
            /*
             *  populate snapshot with more info 
             */
            $logger->Info("populate snapshot from $item");
            /*
             * add to snapshot all files/dirs from current location
             */
		    $snapshot->Populate($item);
            /*
             * store changes after population
             */
            store_snapshot($snapshot, $logger);
	    }
    }

    if(wp_next_scheduled('qtr_internal_scan_cron_hook')){
    	force_next_cron_job();
    }else{
        /*
         * This case may occure when used terminated scan from the dashboard
         */
        $logger->Info(sprintf("Investigation of %s done", ABSPATH));
    }
    return;
}


/**
 * @brief       WordPress cron job callback for heuristic (advanced) file scanning
 * @details     Processes the filesystem snapshot with heuristic detection enabled.
 *              Scans in time-limited batches to prevent system overload.
 *              This is a legacy function; modern code should use settings-based mode instead.
 * @param[in]   $args - (optional) list of input arguments from WordPress cron
 * @return      void - Exits execution when scan completes or times out
 * @see         on_qtr_scanner_internal_scan_cron_event() for settings-based version
 * @see         CQtrFilesScanner for file scanning implementation
 */
function on_qtr_scanner_heur_internal_scan_cron_event($args=NULL)
{
    @set_time_limit(0);
    $logger = new CQtrLogger();
    $snapshot = load_snapshot($logger);
    if( !$snapshot ){
        $logger->Error("Failed to locate filesystem snapshot");
        return;
    }
    $logger->Info(sprintf("Snapshot %d elements", $snapshot->FilesCount()));

    $stime = time();
    $etime = $stime + QTR_SCAN_CRON_TIMEOUT;
    // Force heuristic (advanced) scanning enabled regardless of settings
    $scanner = new CQtrFilesScanner(TRUE/*heuristic*/);
    $scanner->Initialize();

    while(time() < $etime){
        /*
         * runing limited period of time to prevent system overload
         */
        $item = $snapshot->Pop();

        if($item == NULL ){
            /*
             * snapshot is empty, all scanned
             */
            $scanner->Finalize();
            /*
             * remove callback registration
             */
            clean_heur_internal_scan_hook();
            $logger->Info(sprintf("Investigation of %s done", ABSPATH));
            return;
        }

        /*
         * In case scan of next file will take too much and this session will be killed
         */
        store_snapshot($snapshot, $logger);

	    if(is_file($item)){
            #$logger->Info(sprintf("%s Scanning $item", __FUNCTION__));
            $scanner->ScanFile($item);
		}else if($scanner->IsIgnored($item)){
			$logger->Info(sprintf("Skipping %s"));
	    }else{
            /*
             *  populate snapshot with more info 
             */
            $logger->Info("populate snapshot from $item");
		    $snapshot->Populate($item);
            /*
             * store changes after population
             */
            store_snapshot($snapshot, $logger);
	    }
    }

    if(wp_next_scheduled('qtr_heur_internal_scan_cron_hook')){
    	force_next_cron_job();
    }else{
        /*
         * This case may occure when used terminated scan from the dashboard
         */
        $logger->Info(sprintf("High Sensitive Investigation of %s done", ABSPATH));
    }
    return;
}


/**
 * @class CQtrAjaxHandler
 * @brief AJAX request handler for Quttera scanner operations
 * @details Provides static methods for handling AJAX requests from the WordPress admin dashboard.
 *          All public methods verify nonce and user capabilities before execution.
 *          Handles internal/external scanning, threat reporting, whitelisting, and settings management.
 */
class CQtrAjaxHandler
{

    private static function __can_access(){
        $nonce = isset($_REQUEST['_wpnonce']) ? sanitize_text_field($_REQUEST['_wpnonce']) : '';
        if (empty($nonce)) {
            wp_send_json_error(['message' => 'Nonce is missing'], 400);
        }
        if (!wp_verify_nonce($nonce, 'quttera')) {
            wp_send_json_error(['message' => 'Security verification failed. Please refresh the page and try again.'], 403);
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => 'You do not have sufficient permissions to access this page.'], 403);
        }
    }

    /**
     * @brief Updates plugin settings from AJAX POST request
     * @details Validates input using QutteraSettings::ValidateInput(), creates settings object,
     *          and persists to WordPress options via QutteraSettings::Save().
     *          Respects scan_mode setting (normal/heuristic) from QutteraSettings constants.
     * @return void - JSON response (success/error)
     * @throws wp_die() on permission check failure
     * @see QutteraSettings::UpdateSettings() for form submission
     */
    public static function UpdateSettings()
    {
        // Verify permissions and nonce
        self::__can_access();
        
        // Log the incoming POST data for debugging
        $logger = new CQtrLogger();
        $logger->Info('Settings update request received: ' . json_encode($_POST));
                
        // Validate input
        if (!QutteraSettings::ValidateInput($_POST)) {
            $logger->Error('Settings validation failed for: ' . json_encode($_POST));
            wp_send_json_error([
                'message' => 'Settings validation failed. Check error logs for details.'
            ], 400);
            return;
        }

        $logger->Info('Settings validation passed.');
        
        // Create settings from POST data
        $settings = QutteraSettings::CreateFromInput($_POST);
        if (!$settings) {
            $logger->Error('Failed to create settings from POST data');
            wp_send_json_error([
                'message' => 'Failed to create settings. Check error logs for details.'
            ], 400);
            return;
        }
        
        $logger->Info('Settings object created: ' . print_r($settings->GetAll(), true));
        
        // Save to WordPress options
        if (!$settings->Save()) {
            $logger->Error('Failed to save settings to WordPress options');
            wp_send_json_error([
                'message' => 'Failed to save settings to database.'
            ], 500);
            return;
        }
        
        // Send success response
        wp_send_json_success([
            'message' => 'Settings updated successfully',
            'data' => $settings->GetAll()
        ]);
    }

    /**
     * @brief Initiates external cloud-based malware scanning via Quttera API
     * @details Validates domain name and Quttera server URL, then sends scan request.
     *          Supports retry if initial request fails.
     * @return void - JSON response with scan status
     * @see CQtrExternalScanner::SendQuery() for API communication
     */
    public static function RunExternalScan()
    {
        self::__can_access();        
        
        $this_url = sanitize_text_field(trim($_POST['_this'] ?? ''));    /* domain name of this server */
        $qtr_url  = esc_url_raw(trim($_POST['_qtr_url'] ?? ''));         /* quttera investigation server name */

        if( empty($this_url) )
        {
            $this_url = CQtrUtils::GetDomainName();
        }
        else if( empty($qtr_url) )
        {
            $qtr_url = "http://wp.quttera.com";
        }

        if( strpos($this_url, "://" ) != false )
        {
            $parse      = parse_url($this_url);
            $this_url   = $parse['host'];
        }
        /* 
        * validate input of host name 
        */
        if(filter_var(gethostbyname($this_url), FILTER_VALIDATE_IP) === FALSE )
        {
            /* send error to frontend */
            echo json_encode( array( 'content' => array("state" => "Failed to access local server",
                                                        "age" => time(),
                                                        "url" => "localhost" )));
            exit;

        }
        
        /* validate quttera server address */
        if( filter_var($qtr_url, FILTER_VALIDATE_URL) === FALSE )
        {
            /* send error to fronend */
            echo json_encode( array( 'content' => array("state" => "Failed to access remote server",
                                                        "age" => time(),
                                                        "url" => $this_url )));
            exit;
        }

        $investigation_url =  $qtr_url . "/wp_scan/" . $this_url;

        if( filter_var($investigation_url, FILTER_VALIDATE_URL) === FALSE )
        {
            echo json_encode( array( 'content' => array("state" => "Remote server address is invalid",
                                                        "age" => time(),
                                                        "url" => "<undefined>" )));
            exit;
        }

        usleep(1000000); //sleep for a second

        $output = CQtrExternalScanner::SendQuery( $investigation_url );
        
        if( empty($output) )
        {
            $output = CQtrExternalScanner::SendQuery( $investigation_url );
        
            if( $output == false )
            {
                echo json_encode( array( 'content' => array("state" => "Failed to access investigation server [ " . $qtr_url . " ]",
                                                            "age" => time(),
                                                            "url" => $this_url,
                                                            "img" => plugins_url( 'loader.gif', __FILE__ )) 
                                        ) 
                                );
                exit;
            }
        }
        
        $logger = new CQtrLogger();
        $logger->Info(sprintf("External scanner scan-reply: %s",$output));
        
        // Cache scan results to database
        $cache_json = base64_encode($output);
        $rc = CQtrOptions::UpdateOption(QTR_ESCAN_STATUS, $cache_json, $logger);
        if($rc){
            $logger->Info("Cached external scan results to database");
        }else{
            $logger->Error("Failed to cache external scan results to database");
        }

        $output = json_decode($output);
            
        //if state is not finished sleep for a second
        echo json_encode( array( 'content' => $output ) );
        exit;
    }


    public static function IsInternalScanNowRunning()
    {
        self::__can_access(); 
        if( self::IsInternalScanRunning() ){
	        echo "yes";
	    }else{
            echo "no";
        }
        exit();
    }

    
    public static function IsExternalScanNowRunning()
    {
        self::__can_access(); 
        $logger = new CQtrLogger();

        $logger->Info("Checking external scan status from cache if it is running");

        $cache_encoded = CQtrOptions::GetOption(QTR_ESCAN_STATUS, $logger);        
        if (!$cache_encoded) {
            $logger->Error("No cached external scan status found, return No");
            echo "no";
            exit();
        }
        // Decode from base64
        $parser = new QtrExternalScanReportParser();
        $report =  null;
        $cache_json = base64_decode($cache_encoded);
        $report = $parser->parse($cache_json);
            
        if ($report == null || !is_array($report)) {
            $logger->Error("Cannot parse report, return No");
            echo "no";
            exit();
        }   

        // Populate fields from cached report, keeping original values as fallback
        if (isset($report['state']) && $report['state']) {
            $status = strtolower($report['state']);
            $valid_statuses = array('new', 'download', 'downloaded', 'scan', 'scanned');
            if (in_array($status, $valid_statuses)) {
                $logger->Info("External scan is running, return Yes");
                echo "yes";
                exit();
            } 
        }

        $logger->Error("Either scan status is finished or invalid, return No");
        echo "no";
        exit();
    }
    

    /**
     * @brief Initiates internal file scanning using settings-based scan mode
     * @details Loads settings and respects IsHeuristicMode() for determining scan type.
     *          Creates filesystem snapshot, registers WordPress cron job, and returns log output.
     *          Prevents concurrent scans by checking IsInternalScanRunning().
     * @return void - JSON-encoded log lines
     * @see QutteraSettings::IsHeuristicMode() for scan type determination
     * @see schedule_internal_scan_hook() for cron scheduling
     */
    public static function RunInternalScan()
    {
        self::__can_access();
        wp_cache_flush();
        flush();

        $logger = new CQtrLogger();
        $logger->Info(sprintf("Starting investigation of %s",ABSPATH));

        /*
         * Check if any internal scan is running
         */
        if( self::IsInternalScanRunning() ){
            $logger->Info("Error, internal scan process already running");
            $output = $logger->GetAllLines();
            echo json_encode($output);
            exit();
        }
        
        $stats  = new CQtrStats();
        $stats->Reset(); 

        $report = new CQtrReport();
        $report->Reset();

        $logger->Info("Loading settings");
        // Load settings
        $settings = QutteraSettings::LoadFromOptions();
        if (!$settings) {
            $settings = new QutteraSettings();
        }

        // Determine scan mode from settings and initialize scanner accordingly
        $scanner_type = "heuristic";
        if (!$settings->IsHeuristicMode()) {
            $scanner_type = "normal";
        }

        // Create and initialize file scanner with the appropriate scan mode
        $scanner = new CQtrFilesScanner($settings->IsHeuristicMode());
        $scanner->Initialize();

        $path = ABSPATH;
        $fs = new CQtrFsSnapShot();
        $fs->Populate(ABSPATH); 

        $logger->Info(sprintf("Snapshot %d elements, scanner %s", $fs->FilesCount(), $scanner_type));
        /*
         * store initial filesystem snapshot to be scanned
         */
        store_snapshot($fs, $logger);
        /*
         * clean previous cron jobs if exist
         */
        clean_internal_scan_hook();

        /*
         * Starting next scan slot
         */
        schedule_internal_scan_hook();
        $logger->Info("Starting internal scan of [$path]");
        $output = $logger->GetAllLines();
        echo json_encode($output);
        exit();
    }


    /**
     * @brief Initiates heuristic (advanced) file scanning
     * @details Forces heuristic detection enabled regardless of settings.
     *          Creates filesystem snapshot and registers WordPress cron job.
     *          This is a legacy method; consider using RunInternalScan() instead.
     * @return void - JSON-encoded log lines
     * @deprecated Use RunInternalScan() which respects QutteraSettings::IsHeuristicMode()
     * @see RunInternalScan() for settings-based approach
     */
    public static function RunHeurInternalScan()
    {
        self::__can_access();
        wp_cache_flush();
        flush();

        $logger = new CQtrLogger();
        $logger->Info(sprintf("Starting investigation of %s",ABSPATH));

        /*
         * Check if any internal scan is running
         */
        if( self::IsInternalScanRunning() ){
            $logger->Error("Error, internal scan process already running");
            $output = $logger->GetAllLines();
            echo json_encode($output);
            exit();
        }
        
        $stats  = new CQtrStats();
        $stats->Reset(); 

        $report = new CQtrReport();
        $report->Reset();

        $scanner = new CQtrFilesScanner(TRUE/*heuristic*/);
        $scanner->Initialize();

        $path = ABSPATH;
        $fs = new CQtrFsSnapShot();
        $fs->Populate(ABSPATH); 
        /*
         * store initial filesystem snapshot to be scanned
         */
        store_snapshot($fs, $logger);
        /*
         * clean previous cron jobs if exist
         */
        clean_heur_internal_scan_hook();

        schedule_heur_internal_scan_hook();
        $logger->Info("Starting high sensitive internal scan of [$path]");
        $output = $logger->GetAllLines();
        echo json_encode($output);
        exit();
    }


    /**
     * @brief Terminates any running internal scan
     * @details Removes both normal and heuristic scan cron hooks from WordPress scheduler.
     * @return void - Log output via exit()
     * @see clean_internal_scan_hook() removes normal scan cron
     * @see clean_heur_internal_scan_hook() removes heuristic scan cron
     */
    public static function StopInternalScan()
    {
        self::__can_access();
        $logger = new CQtrLogger();
        $logger->Info("Handling request to terminate internal scan");
        $logger->Info("Remove cron job from scheduler");

        wp_cache_flush();

        clean_internal_scan_hook();

        clean_heur_internal_scan_hook();

        if(wp_next_scheduled('qtr_internal_scan_cron_hook')){
            $logger->Error("Failed to remove scanner event from cron schedule");
        }else{
            $logger->Info("Cron job cleared");
        }

        exit();
    }

    public static function IsInternalScanRunning()
    {
        self::__can_access();
        wp_cache_flush();

        $timestamp = wp_next_scheduled('qtr_internal_scan_cron_hook');
        if($timestamp){
            return TRUE;
        }

        $timestamp = wp_next_scheduled('qtr_heur_internal_scan_cron_hook');
        if(!$timestamp){
            return FALSE;
        }

        return TRUE;
    }

    public static function GetLogLines()
    {
        self::__can_access();
        $index  = 0;
        $logger = new CQtrLogger();
        
        if( isset( $_GET['start_line']) ) 
        {
            $index = intval( $_GET['start_line']);
        }

        else if( isset( $_POST['start_line']) ) 
        {
            $index = intval( $_POST['start_line']);
        }

        $lines = $logger->GetAllLines();
        echo json_encode($lines);
        exit();
    }

    public static function CleanLogLines()
    {
        self::__can_access();
        $index  = 0;
        $logger = new CQtrLogger();
        $logger->Clean();        
        exit();
    }


    public static function GetStats()
    {
        self::__can_access();
        wp_cache_flush();
        $report = new CQtrReport();
        $stats  = $report->GetStats();
        $counters = $stats->GetCounters();
        echo json_encode($counters);
        exit();
    }

    public static function ScannerReport()
    {
        self::__can_access();
        $report = new CQtrReport();
        $header = $report->GenerateMeta();
        $dump   = $report->GetDetectedThreats();
        $body = "";
        foreach ( $dump  as $entry){
            $threat = $entry["THREAT"];
            $threat = preg_replace("/\s\s*/"," ", $threat);
            $threat = preg_replace("/\r\n/","", $threat);
            $body .= "\r\n\r\n";
            $body .= "FILE:         " . $entry["FILE"] . "\r\n";
            $body .= "FILE_MD5:     " . $entry["FILE_MD5"] . "\r\n";
            $body .= "SEVERITY:     " . $entry["SEVERITY"] . "\r\n";
            $body .= "ENGINE:       " . $entry["ENGINE"] . "\r\n";
            $body .= "THREAT_SIG:   " . $entry["THREAT_SIG"] . "\r\n";
            $body .= "THREAT_NAME:  " . $entry["THREAT_NAME"] . "\r\n";
            $body .= "THREAT:       " . $threat . "\r\n";
            $body .= "DETAILS:      " . $entry["DETAILS"] . "\r\n";
        }

        echo $header . "\r\n" . $body;
        exit();
    }

    public static function GetDetectedThreatsReport()
    {
        self::__can_access();
        $report = new CQtrReport();
        $output = $report->GetDetectedThreats();
        echo json_encode($output);
        exit();
    }

    public static function ShowFile()
    {
        self::__can_access();
        $file_hash   = "";
        $output = "";
        $logger = new CQtrLogger();

        if(!isset( $_POST["FILE"] ) ){ 
            wp_die(__('File identifier is missing.'));         
        }

        if(!preg_match('/^[a-f0-9]{32}$/i', $_POST["FILE"])){
            wp_die(__('File identifier is invalid.'));         
        }

        // Properly capture and sanitize the file hash from POST
        $file_hash = sanitize_text_field($_POST["FILE"]);

        $report = new CQtrReport();
        $dump   = $report->GetDetectedThreats();

        $file   = "";         
        foreach ( $dump  as $entry){
            if( strcasecmp($entry["FILE_MD5"], $file_hash) === 0){
                $file = ABSPATH . $entry["FILE"];
                break;
            }
        }

        if(strlen($file) === 0){
            $logger->Error("Failed to locate the required file");
            wp_die(__('You do not have sufficient permissions to access this page.'));         
        }

        $logger->Info("Showing file $file");

        if(!is_file($file)){
            $logger->Error("Failed to locate required file [$file]");
            exit();
        }

        //verifing that referenced file is part of the WP installation
        $real_path = realpath($file);
        if(strpos($real_path,ABSPATH,0) !== 0){
            $logger->Error("Permission denied [$real_path]");
            wp_die(__('You do not have sufficient permissions to access this page.'));         
        }

        $output = file_get_contents($file);
        header('Content-Type: text/plain; charset=UTF-8');
        header('X-Content-Type-Options: nosniff');        
        echo $output;
        exit();
    } 

    public static function GetIgnoredThreatsReport()
    {
        self::__can_access();
        $ignore_list    = new CQtrIgnoreList();
        $report         = new CQtrReport( );
        $threats        = $report->Get();
        $output         = array();

        /* 
         * remove all not-ignored threats 
         */
        foreach( $threats as $threat )
        {
            /*
             * if threat is not part of the ignored list, remove it from output
             */
            if( $ignore_list->Get( $threat["FILE_MD5"], $threat["THREAT_SIG"] ) )
            {
                array_push($output,$threat);
            }
        }

        echo json_encode($output);
        exit();
    }


    public static function IgnoreThreat()
    {
        self::__can_access();
        $file   = "";
        $threat = "";

        if( isset( $_POST["FILE_MD5"] ) ){
            $file = sanitize_text_field($_POST["FILE_MD5"]);
            // Validate MD5 format
            if (!empty($file) && !preg_match('/^[a-f0-9]{32}$/i', $file)) {
                wp_send_json_error(['message' => 'Invalid file identifier format'], 400);
            }
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = sanitize_text_field($_POST["THREAT_SIG"]);
            // Validate threat signature format (alphanumeric with underscores/dashes)
            if (!empty($threat) && !preg_match('/^[a-zA-Z0-9_\-\.]+$/', $threat)) {
                wp_send_json_error(['message' => 'Invalid threat signature format'], 400);
            }
        }

        $ignore_list = new CQtrIgnoreList();
        if( !$ignore_list->Add($file,$threat) ){
            echo json_encode("Operation failed");
        }else if( $ignore_list->Get($file,$threat) == NULL ) {
            echo json_encode("Failed to retrieve just added threat for $file:$threat");
        }else{
            echo json_encode("Operation succeeded"); 
        }
        exit();
    }


    public static function RemoveFromIgnoreList()
    {
        self::__can_access();
        $file   = "";
        $threat = "";

        if( isset( $_POST["FILE_MD5"] ) ){
            $file = sanitize_text_field($_POST["FILE_MD5"]);
            if (!empty($file) && !preg_match('/^[a-f0-9]{32}$/i', $file)) {
                wp_send_json_error(['message' => 'Invalid file identifier format'], 400);
            }
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = sanitize_text_field($_POST["THREAT_SIG"]);
            if (!empty($threat) && !preg_match('/^[a-zA-Z0-9_\-\.]+$/', $threat)) {
                wp_send_json_error(['message' => 'Invalid threat signature format'], 400);
            }
        }

        $ignore_list = new CQtrIgnoreList();
        if( !$ignore_list->Remove($file,$threat) ){
            echo json_encode("Operation failed");
        }else{
            echo json_encode("Operation succeeded"); 
        }
        exit();
    }

    public static function CleanIgnoreList()
    {
        self::__can_access();
        $ignore_list = new CQtrIgnoreList();
        $ignore_list->Clean();
        echo json_encode("Operation succeeded");
        exit();
    }


    public static function AcknowledgeAlarm()
    {
        $logger = new CQtrLogger();
        $logger->Info("=== AcknowledgeAlarm handler called ===");
        $logger->Info("REQUEST METHOD: " . $_SERVER['REQUEST_METHOD']);
        $logger->Info("POST data: " . json_encode($_POST));
        
        // Verify nonce and permissions
        self::__can_access();
        
        if (!isset($_POST['alarm_id'])) {
            $logger->Error("alarm_id parameter missing");
            wp_send_json_error(['message' => 'alarm_id parameter missing'], 400);
        }
        
        $alarm_id = intval($_POST['alarm_id']);
        $user_id = get_current_user_id();
        
        try {
            $logger->Info("Acknowledging alarm $alarm_id by user $user_id");
            CQtrAlarmsManager::acknowledge($alarm_id, $user_id);
            $logger->Info("Alarm $alarm_id acknowledged successfully");
            wp_send_json_success(['message' => "Alarm $alarm_id acknowledged successfully"]);
        } catch (Exception $e) {
            $logger->Error("Failed to acknowledge alarm $alarm_id: " . $e->getMessage());
            wp_send_json_error(['message' => 'Failed to acknowledge alarm'], 500);
        }
    }

    public static function GetAlarms()
    {
        $logger = new CQtrLogger();
        $logger->Info("=== GetAlarms handler called ===");
        
        // Verify nonce and permissions
        self::__can_access();
        
        try {
            $limit = isset($_POST['limit']) ? intval($_POST['limit']) : 10;
            $only_unacked = isset($_POST['only_unacked']) ? (bool) $_POST['only_unacked'] : false;
            
            $logger->Info("Fetching alarms: limit=$limit, only_unacked=$only_unacked");
            $alarms_raw = CQtrAlarmsManager::get_alarms($limit, true);
            
            // Format alarms for JavaScript
            $formatted_alarms = [];
            if (!empty($alarms_raw)) {
                foreach ($alarms_raw as $alarm) {
                    $formatted_alarms[] = [
                        'id' => $alarm['id'],
                        'severity' => ucfirst($alarm['severity']),
                        'type' => 'Alert',
                        'target' => wp_kses_post(substr($alarm['body'], 0, defined('ALARM_BODY_PREVIEW_LENGTH') ? ALARM_BODY_PREVIEW_LENGTH : 100)),
                        'time' => human_time_diff(strtotime($alarm['created_at']), current_time('timestamp')) . ' ago',
                        'status' => $alarm['ack'] == 0 ? 'Open' : 'Acknowledged',
                        'created_at' => $alarm['created_at'],
                    ];
                }
            }
            
            $logger->Info("Successfully fetched and formatted " . count($formatted_alarms) . " alarms");
            wp_send_json_success($formatted_alarms);
        } catch (Exception $e) {
            $logger->Error("Failed to fetch alarms: " . $e->getMessage());
            wp_send_json_error(['message' => 'Failed to fetch alarms'], 500);
        }
    }

    public static function WhiteListThreat()
    {
        self::__can_access();
        $file   = "";
        $threat = "";

        if( isset( $_POST["FILE_MD5"] ) ){
            $file = sanitize_text_field($_POST["FILE_MD5"]);
            if (!empty($file) && !preg_match('/^[a-f0-9]{32}$/i', $file)) {
                wp_send_json_error(['message' => 'Invalid file identifier format'], 400);
            }
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = sanitize_text_field($_POST["THREAT_SIG"]);
            if (!empty($threat) && !preg_match('/^[a-zA-Z0-9_\-\.]+$/', $threat)) {
                wp_send_json_error(['message' => 'Invalid threat signature format'], 400);
            }
        }

        $ignore_list = new CQtrIgnoreList();
        $white_list  = new CQtrThreatsWhiteList();
        /*
         * Remove threat from ignored list if it is there
         */
        $ignore_list->Remove($file,$threat);

        if( !$white_list->Add($file,$threat) ){
            echo json_encode("Operation failed");
        }else if( $white_list->Get($file,$threat) == NULL ) {
            echo json_encode("Failed to retrieve just added threat for $file:$threat");
        }else{
            echo json_encode("Operation succeeded for $file:$threat"); 
        }
        exit();
    }


    public static function CleanThreatsWhiteList()
    {
        self::__can_access();
        $white_list = new CQtrThreatsWhiteList();
        $white_list->Clean();
        echo json_encode("Operation succeeded");
        exit();
    }


    public static function WhiteListFile()
    {
        self::__can_access();
        $file_path  = NULL;
        $file_sig   = NULL;

        if( isset($_POST["FILE_MD5"] ) ) {
            $file_sig = sanitize_text_field(trim($_POST["FILE_MD5"]));
            if (!empty($file_sig) && !preg_match('/^[a-f0-9]{32}$/i', $file_sig)) {
                wp_send_json_error(['message' => 'Invalid file signature format'], 400);
            }
        }
        
        if( isset($_POST["FILE"]) ){
            $file_path = sanitize_text_field(trim($_POST["FILE"]));
            // Validate file path - should be relative path within ABSPATH
            if (!empty($file_path)) {
                $real_path = realpath(ABSPATH . $file_path);
                if ($real_path === false || strpos($real_path, ABSPATH) !== 0) {
                    wp_send_json_error(['message' => 'Invalid file path'], 400);
                }
            }
        }

        $rc = FALSE;
        $white_list = new CQtrFilesWhiteList();
        $white_list->Load();
        $error = "operation succeeded";
        if( $file_sig ){
            $rc = $white_list->AddBySig( $file_sig );
            if( !$rc ){

                if( $white_list->IsWhiteListed( $file_sig ) ){
                    $error = "$file_sig already whitelisted";
                }else{
                    $error = "$file_sig failed due to invalid input";
                }
            }
        }else{
            $rc = $white_list->AddByPath( $file_path );
            if( !$rc ){
                if( $white_list->IsWhiteListedFile( $file_path ) ){
                    $error = "$file_path already whitelisted";
                }else{
                    $error = "$file_path failed due to invalid input";
                }
            }
        }

        echo json_encode($error);
        exit();
    }


    public static function  CleanFilesWhiteList()
    {   
        self::__can_access();
        $white_list = new CQtrFilesWhiteList();
        if( $white_list->Clean() ){
            echo json_encode("Operation succeeded");
        } else {
            echo json_encode("Operation failed");
        }
        exit();
    }

    /**
     * AJAX Handler: Get brute-force blocked IPs
     * Returns list of currently locked IPs with expiry info
     */
    public static function GetBruteForceBlockedIPs() {
        // Verify nonce and user capability
        self::__can_access();
        // Get all active locked IPs from brute-force cache
        $active_locked_ips = CQtrBruteForce::GetActiveLockedIPs();
        // Format for frontend
        $blocked_ips = array();
        foreach ($active_locked_ips as $ip => $lock_data) {
            $blocked_ips[] = array(
                'ip' => $ip,
                'locked_full' => $lock_data['locked_full'],
                'expiry_full' => $lock_data['expiry_full'],
                'expiry_relative' => $lock_data['expiry_relative'],
                'locked_at' => $lock_data['locked_at'],
                'expires_at' => $lock_data['expires_at'],
            );
        }

        wp_send_json_success(array(
            'blocked_ips' => $blocked_ips,
            'timestamp' => current_time('mysql'),
        ));
    }

    /**
     * AJAX Handler: Remove a specific brute-force blocked IP
     */
    public static function RemoveBruteForceBlockedIP() {
        // Verify nonce and user capability
        self::__can_access();
        
        $ip = isset($_POST['ip']) ? sanitize_text_field($_POST['ip']) : '';
        if (empty($ip)) {
            wp_send_json_error(['message' => 'IP not provided'], 400);
        }

        if (!CQtrIPManager::IsValidIP($ip)) {
            wp_send_json_error(['message' => 'Invalid IP address format'], 422);
        }

        // Call public API to unlock the IP
        $unlocked = CQtrBruteForce::UnlockIP($ip);
        
        if (!$unlocked) {
            wp_send_json_error(['message' => 'IP was not locked or unlock failed'], 404);
        }
        
        wp_send_json_success(array('message' => 'IP block removed', 'ip' => $ip));
    }

    /**
     * AJAX Handler: Clear all brute-force blocked IPs
     */
    public static function ClearAllBruteForceBlockedIPs() {
        // Verify nonce and user capability
        self::__can_access();

        // Get all currently active locked IPs
        $active_locked_ips = CQtrBruteForce::GetActiveLockedIPs();
        $count = count($active_locked_ips);

        // Clear each IP lock using public API
        if ($count > 0) {
            foreach ($active_locked_ips as $ip => $lock_data) {
                CQtrBruteForce::UnlockIP($ip);
            }
        }

        wp_send_json_success(array(
            'message' => 'All brute-force IP blocks cleared',
            'count' => $count,
        ));
    }

    /**
     * AJAX Handler: Get bot protection blocked IPs
     * Returns list of currently blocked client keys with risk info
     * 
     * Security:
     *  - Verifies nonce
     *  - Checks user capability (manage_options)
     * 
     * Response:
     *  {
     *    "success": true,
     *    "data": {
     *      "blocked_clients": [
     *        { "client_key": "...", "risk": 18, "expiry_relative": "10m", ... }
     *      ],
     *      "count": 1,
     *      "timestamp": "2026-01-13 14:55:00"
     *    }
     *  }
     */
    public static function GetBotProtectionBlockedIPs() {
        self::__can_access();
        $active_blocked = CQtrBotProtection::GetActiveLockedIPs();
        $blocked_clients = array();
        foreach ($active_blocked as $client_key => $block_data) {
            $blocked_clients[] = array(
                'client_key'      => $client_key,
                'ip'              => isset($block_data['ip']) ? $block_data['ip'] : 'Unknown',
                'blocked_at'      => date('Y-m-d H:i:s', $block_data['blocked_at']),
                'expires_at'      => date('Y-m-d H:i:s', $block_data['expires_at']),
                'expiry_relative' => $block_data['expiry_relative'],
                'ttl'             => $block_data['ttl'],
                'risk'            => $block_data['risk'],
                'reasons'         => $block_data['reasons'],
                'status'          => $block_data['status'],
            );
        }
        wp_send_json_success(array(
            'blocked_clients' => $blocked_clients,
            'count'          => count($blocked_clients),
            'timestamp'      => current_time('mysql'),
        ));
    }

    /**
     * AJAX Handler: Remove a specific bot protection blocked IP
     * Clears the block for a single client key
     * 
     * Security:
     *  - Verifies nonce
     *  - Validates client key format (32-char hex)
     *  - Checks user capability
     */
    public static function RemoveBotProtectionBlockedIP() {
        self::__can_access();

        $client_key = isset($_POST['client_key']) ? sanitize_text_field($_POST['client_key']) : '';
        if (empty($client_key)) {
            wp_send_json_error(['message' => 'Client key not provided'], 400);
        }

        if (!preg_match('/^[a-f0-9]{32}$/i', $client_key)) {
            wp_send_json_error(['message' => 'Invalid client key format'], 422);
        }

        $cleared = CQtrBotProtection::ClearBotBlock($client_key);
        
        if (!$cleared) {
            wp_send_json_error(['message' => 'Client was not blocked or unlock failed'], 404);
        }
        wp_send_json_success(array(
            'message'    => 'Bot protection block removed',
            'client_key' => $client_key,
        ));
    }

    /**
     * AJAX Handler: Clear all bot protection blocked IPs
     * Removes all currently active bot protection blocks
     */
    public static function ClearAllBotProtectionBlockedIPs() {
        self::__can_access();
        $active_blocked = CQtrBotProtection::GetActiveLockedIPs();
        $count = count($active_blocked);
        if ($count > 0) {
            foreach ($active_blocked as $client_key => $block_data) {
                CQtrBotProtection::ClearBotBlock($client_key);
            }
        }
        wp_send_json_success(array(
            'message' => 'All bot protection blocks cleared',
            'count'   => $count,
        ));
    }

    /**
     * API: Get Brute-Force Protection Statistics
     * 
     * Returns comprehensive statistics including:
     *  - Status (Enabled/Disabled)
     *  - Blocked IPs count
     *  - Expiring Soon count (within 5 minutes)
     * 
     * Response:
     *  {
     *    "success": true,
     *    "data": {
     *      "status": "Enabled" or "Disabled",
     *      "blocked_ips": 5,
     *      "expiring_soon": 2,
     *      "timestamp": "2026-01-15 11:05:00"
     *    }
     *  }
     */
    public static function GetBruteForceStats() {
        // Verify nonce and user capability
        self::__can_access();

        // Get brute-force settings
        $settings_obj = QutteraSettings::LoadFromOptions();
        $settings = $settings_obj ? $settings_obj->GetAll() : array();
        $is_enabled = isset($settings['bruteforce_enabled']) && $settings['bruteforce_enabled'];

        // Get all active locked IPs
        $active_locked_ips = CQtrBruteForce::GetActiveLockedIPs();
        $total_blocked = count($active_locked_ips);

        // Calculate expiring soon (within 5 minutes)
        $expiring_soon = 0;
        $now = current_time('timestamp');
        foreach ($active_locked_ips as $ip => $lock_data) {
            if (isset($lock_data['expires_at'])) {
                $expires = strtotime($lock_data['expires_at']);
                $time_until_expiry = $expires - $now;
                // If expiring within 5 minutes (300 seconds) and not already expired
                if ($time_until_expiry <= (5 * 60) && $time_until_expiry > 0) {
                    $expiring_soon++;
                }
            }
        }

        wp_send_json_success(array(
            'status' => $is_enabled ? 'Enabled' : 'Disabled',
            'blocked_ips' => $total_blocked,
            'expiring_soon' => $expiring_soon,
            'timestamp' => current_time('mysql'),
        ));
    }

    /**
     * API: Get Bot Protection Statistics
     * 
     * Returns comprehensive statistics including:
     *  - Status (Enabled/Disabled)
     *  - Blocked Clients count
     *  - Critical Risk count (risk >= 18)
     *  - Expiring Soon count (within 5 minutes)
     * 
     * Response:
     *  {
     *    "success": true,
     *    "data": {
     *      "status": "Enabled" or "Disabled",
     *      "blocked_clients": 3,
     *      "critical_risk": 1,
     *      "expiring_soon": 1,
     *      "timestamp": "2026-01-15 11:05:00"
     *    }
     *  }
     */
    public static function GetBotProtectionStats() {
        // Verify nonce and user capability
        self::__can_access();

        // Get bot protection settings
        $settings_obj = QutteraSettings::LoadFromOptions();
        $settings = $settings_obj ? $settings_obj->GetAll() : array();
        $is_enabled = isset($settings['bot_protection_enabled']) && $settings['bot_protection_enabled'];

        // Get all active blocked clients
        $blocked_clients = CQtrBotProtection::GetActiveLockedIPs();
        $total_blocked = count($blocked_clients);

        // Calculate critical risk (risk >= 18) and expiring soon (within 5 minutes)
        $critical_risk = 0;
        $expiring_soon = 0;
        $now = current_time('timestamp');

        foreach ($blocked_clients as $client_key => $client) {
            // Count critical risk clients
            if (isset($client['risk'])) {
                $risk = intval($client['risk']);
                if ($risk >= 18) {
                    $critical_risk++;
                }
            }

            // Count expiring soon
            if (isset($client['expires_at'])) {
                $expires = strtotime($client['expires_at']);
                $time_until_expiry = $expires - $now;
                // If expiring within 5 minutes (300 seconds) and not already expired
                if ($time_until_expiry <= (5 * 60) && $time_until_expiry > 0) {
                    $expiring_soon++;
                }
            }
        }

        wp_send_json_success(array(
            'status' => $is_enabled ? 'Enabled' : 'Disabled',
            'blocked_clients' => $total_blocked,
            'critical_risk' => $critical_risk,
            'expiring_soon' => $expiring_soon,
            'timestamp' => current_time('mysql'),
        ));
    }

}

?>
