<?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');
require_once('qtrAjaxTimerGuard.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 - 10 );

/** Option key used to persist the DB scan snapshot between cron firings. */
define( 'QTR_DB_SCAN_CRON_ARGS', 'qtr_db_scan_cron_args' );

/** Rows fetched per ScanBatch() call. */
if ( !defined('QTR_DB_SCAN_BATCH_SIZE') ) { define( 'QTR_DB_SCAN_BATCH_SIZE', 50 ); }

/** Maximum column value size in bytes before it is skipped (512 KB). */
if ( !defined('QTR_DB_MAX_COL_SIZE') ) { define( 'QTR_DB_MAX_COL_SIZE', 512 * 1024 ); }
define('QTR_SCAN_PROGRESS_TRANSIENT', 'qtr_scan_in_progress_timestamp');

/*
 * 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' );

/*
 * maps cron hook to the DB/transients scanner — scheduled automatically when file scan finishes
 */
add_action( 'qtr_db_scan_cron_hook', 'on_qtr_scanner_db_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()
{
    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' )));
}

/**
 * @brief   removes all instances of registered cron job
 * @return  nothing 
 */
function clean_heur_internal_scan_hook()
{
    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' )));
}


function schedule_internal_scan_hook()
{
    /*
     * submit cron job event to run internal scan
     */
    wp_schedule_event( time(), '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(), 'qtrScanPeriod', 'qtr_heur_internal_scan_cron_hook');
}

/**
 * @brief   Stores the DB scan snapshot in wp_options for the next cron firing.
 * @param   CQtrDbSnapshot $snapshot Snapshot object to persist
 * @param   CQtrLogger     $logger
 * @return  bool
 */
function store_db_snapshot($snapshot, $logger)
{
    require_once('qtrDbSnapshot.php');
    $data = $snapshot->ToString();
    if (!$data) {
        $logger->Error('DB scanner: failed to serialize snapshot');
        return false;
    }
    if (get_option(QTR_DB_SCAN_CRON_ARGS) !== false) {
        $rc = update_option(QTR_DB_SCAN_CRON_ARGS, $data, false);
    } else {
        $rc = add_option(QTR_DB_SCAN_CRON_ARGS, $data, '', 'no');
    }
    if (!$rc) { $logger->Error('DB scanner: failed to store snapshot'); }
    return $rc;
}

/**
 * @brief   Loads the DB scan snapshot from wp_options.
 * @param   CQtrLogger $logger
 * @return  CQtrDbSnapshot|null
 */
function load_db_snapshot($logger)
{
    require_once('qtrDbSnapshot.php');
    $data = get_option(QTR_DB_SCAN_CRON_ARGS);
    if (!$data) {
        return null;
    }
    $snapshot = new CQtrDbSnapshot();
    if (!$snapshot->FromString($data)) {
        $logger->Error('DB scanner: failed to deserialize snapshot');
        return null;
    }
    return $snapshot;
}

/**
 * @brief   Schedules the DB scan cron hook to fire ~10 seconds from now.
 */
function schedule_db_scan_hook()
{
    wp_schedule_event(time() + 10, 'qtrScanPeriod', 'qtr_db_scan_cron_hook');
    $logger = new CQtrLogger();
    $logger->Info(sprintf(
        'DB scan scheduled. Next run %s',
        gmdate('H:i:s', wp_next_scheduled('qtr_db_scan_cron_hook'))
    ));
}

/**
 * @brief   Removes all instances of the DB scan cron hook.
 */
function clean_db_scan_hook()
{
    wp_cache_flush();
    do {
        $timestamp = wp_next_scheduled('qtr_db_scan_cron_hook');
        if ($timestamp) {
            wp_unschedule_event($timestamp, 'qtr_db_scan_cron_hook');
        }
    } while ($timestamp = wp_next_scheduled('qtr_db_scan_cron_hook'));
    wp_cache_flush();
}


/**
 * @brief   Check if a scan is running using timestamp-based lock
 * @details Compares current time against last scan start timestamp.
 *          If last scan started > QTR_SCAN_CRON_TIMEOUT seconds ago, it's considered stale.
 *          This is self-healing: after timeout period, system auto-recovers even if transient isn't cleaned.
 * @return  bool - TRUE if scan is actively running, FALSE if safe to start new scan
 */
function is_scan_in_progress() {
    $last_start = get_transient(QTR_SCAN_PROGRESS_TRANSIENT);
    
    if ($last_start === false) {
        // No transient = no scan running
        return false;
    }
    
    // Check if last scan is older than timeout period
    // If it is, the previous scan must have completed or crashed
    $elapsed = time() - $last_start;
    
    if ($elapsed > QTR_SCAN_CRON_TIMEOUT) { 
        // Adding buffer to ensure we don't prematurely consider a scan stale 
        // Stale lock - previous scan is done (completed or crashed)
        return false;
    }
    
    // Scan is actively running (within timeout window)
    return true;
}

/**
 * @brief   Mark scan as started with current timestamp
 * @details Sets transient with auto-expiry. TTL is longer than QTR_SCAN_CRON_TIMEOUT
 *          so timestamp persists across multiple cron cycles if needed.
 */
function mark_scan_started() {
    $current_time = time();
    // TTL: buffer + 1 minute
    $ttl = QTR_SCAN_CRON_TIMEOUT + 60;
    set_transient(QTR_SCAN_PROGRESS_TRANSIENT, $current_time, $ttl);
}

/**
 * @brief   Clear scan progress marker
 */
function clear_scan_progress() {
    delete_transient(QTR_SCAN_PROGRESS_TRANSIENT);
}

/**
 * @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();
}

/**
 * File scan cron job callback - Phase 1 of 2-phase scanning
 * 
 * Processes filesystem snapshot incrementally in time-limited batches (~295 seconds).
 * Respects settings (normal vs heuristic scan mode). When all files scanned, chains
 * to DB scan phase via schedule_db_scan_hook().
 * 
 * Concurrency: Uses transient-based locking with TTL to prevent overlapping scans.
 * Lock self-heals after QTR_SCAN_CRON_TIMEOUT + 30s if scan crashes.
 */
function on_qtr_scanner_internal_scan_cron_event($args=NULL)
{
    @set_time_limit(0);

    $logger = new CQtrLogger();

    // CHECK: Is another scan already running (file or DB phase)?
    // Uses timestamp transient set by mark_scan_started() with auto-expiry TTL.
    if (is_scan_in_progress()) {
        $last_start = get_transient(QTR_SCAN_PROGRESS_TRANSIENT);
        $elapsed = time() - $last_start;
        $logger->Debug("Scan already in progress (started $elapsed seconds ago). Skipping this cron cycle.");
        return;  // Skip this cron execution - will retry in 5 minutes
    }

    // MARK: Start of new scan cycle
    // Sets transient lock with TTL > QTR_SCAN_CRON_TIMEOUT to persist across multiple cron firings.
    // Lock remains active through DB scan phase and is cleared when DB scan completes.
    mark_scan_started();
    $logger->Debug("Starting new scan cycle - timestamp lock set");
    
    // UPDATE: Set scan state to files_scanning
    $stats = new CQtrStats();
    $stats->State('files_scanning');

    // Load filesystem snapshot from previous cron cycle or initial scan request
    $snapshot = load_snapshot($logger);
    if( !$snapshot ){
        $logger->Error("Failed to locate filesystem snapshot");
        clear_scan_progress();  // Clean up lock if snapshot missing
        return;
    }
    
    // Load settings to determine scan mode (normal vs heuristic detection)
    $settings = QutteraSettings::LoadFromOptions();
    if (!$settings) {
        $settings = new QutteraSettings();
    }

    // Instantiate file scanner with scan mode from settings.
    // true = heuristic (advanced) mode, false = normal mode
    $scanner = new CQtrFilesScanner($settings->IsHeuristicMode());
    $scanner->Initialize();
    
    $logger->Info(sprintf("Snapshot %d elements, scanner %s", 
        $snapshot->FilesCount(), 
        $settings->IsHeuristicMode() ? 'heuristic' : 'normal'));

    // Set time window for this cron cycle
    // QTR_SCAN_CRON_TIMEOUT = 5*60 - 5 = 295 seconds
    // Ensures completion before next cron fire in 5 minutes
    $stime = time();
    $etime = $stime + QTR_SCAN_CRON_TIMEOUT;

    // Main processing loop: run until timeout to prevent system overload
    while(time() < $etime){
        // Pop next item from snapshot queue (file or directory path)
        $item = $snapshot->Pop();

        if($item == NULL ){
            /*
             * File scan snapshot is empty - all files have been scanned.
             * Now chain into DB + transients scan phase.
             * Keep transient lock active during DB scan (don't clear progress yet).
             */
            $logger->Debug("File scan snapshot exhausted - chaining to DB scan phase");
            $scanner->Finalize();
            
            // UPDATE: Set scan state to database_scanning
            $stats = new CQtrStats();
            $stats->State('database_scanning');

            /*
             * Initialize DB scan snapshot and schedule it.
             * If DB scan fails to initialize, clear the scan lock to allow retry.
             */
            $logger->Info(sprintf("File scan of %s complete. Starting database scan.", ABSPATH));
            
            require_once('qtrDbSnapshot.php');
            $db_snapshot = new CQtrDbSnapshot();
            if ($db_snapshot->InitTables()) {
                // DB snapshot initialized successfully - persist and schedule
                store_db_snapshot($db_snapshot, $logger);
                schedule_db_scan_hook();
                $logger->Info(sprintf('DB scan scheduled. Tables to scan: %d', $db_snapshot->TablesRemaining()));
                // Transient lock remains active - DB scanner will clear it when complete
            } else {
                // DB initialization failed - clear scan lock to allow retry
                $logger->Error('DB scan: failed to enumerate database tables');
                clear_scan_progress();
            }

            clean_internal_scan_hook();  // Remove file scan cron hook
            return;
        }

        // Persist snapshot after each iteration
        // Allows recovery from crash/timeout mid-batch
        store_snapshot($snapshot, $logger);

	    if(is_file($item)){
            // Item is a file - scan it
			$scanner->ScanFile($item);
		}else if($scanner->IsIgnored($item)){
            // Item is directory in ignore list - skip it
			$logger->Info("Skipping ignored $item");
	    }else{
            // Item is directory - recursively populate snapshot with subdirs/files
            // This incremental approach prevents memory overload from loading entire tree at once
            $logger->Info("Populating snapshot from $item");
		    $snapshot->Populate($item);
            // Persist changes for recovery
            store_snapshot($snapshot, $logger);
	    }
    }

    // Time window exhausted - more items to process
    // Cron job will fire again in 5 minutes to continue
    $next_scan = wp_next_scheduled('qtr_internal_scan_cron_hook');
    if( $next_scan ){
        $next_time_readable = date('Y-m-d H:i:s', $next_scan);
        $seconds_until = $next_scan - time();
        $logger->Info(sprintf('Time window exhausted, forcing next cron cycle to continue file scan. Next cycle scheduled in %d seconds at %s', $seconds_until, $next_time_readable));
        force_next_cron_job();
        clear_scan_progress();  // Clear lock to allow next cycle to start - next cycle will set it again
    }else{
        // Cron hook was unscheduled (user stopped scan from dashboard)
        $logger->Info(sprintf("File scan time window exhausted, but cron unscheduled. Cleanup required."));
        clear_scan_progress();  // Clean up lock
    }
    
    // NOTE: Do NOT clear_scan_progress() here - transient persists to block concurrent scans
    // Transient will remain active through DB scan phase
    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
 */
/*
NOT IN USE ANYMORE - REPLACED BY on_qtr_scanner_internal_scan_cron_event() WHICH RESPECTS SETTINGS
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){
        // running limited period of time to prevent system overload
        $item = $snapshot->Pop();

        if($item == NULL ){
            // snapshot is empty, all scanned — chain into DB + transients scan
            $scanner->Finalize();
            clean_heur_internal_scan_hook();
            $logger->Info(sprintf("File scan of %s complete. Starting database scan.", ABSPATH));

            require_once('qtrDbSnapshot.php');
            $db_snapshot = new CQtrDbSnapshot();
            if ($db_snapshot->InitTables()) {
                store_db_snapshot($db_snapshot, $logger);
                schedule_db_scan_hook();
                $logger->Info(sprintf('DB scan scheduled. Tables to scan: %d', $db_snapshot->TablesRemaining()));
            } else {
                $logger->Error('DB scan: failed to enumerate database tables');
            }
            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", $item));
	    }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;
}
*/

/**
 * @brief   WordPress cron callback: DB table + transients scanner
 * @details Processes the DB snapshot in time-limited batches (295 s) per cron firing.
 *          Runs automatically after the file scan completes — not triggered by the user.
 *          Phase 'tables'     : scans all database tables row by row (50 rows/batch).
 *          Phase 'transients' : scans _transient_* entries in wp_options (50 rows/batch).
 *          Chains phases internally via CQtrDbSnapshot::SetPhase().
 */
function on_qtr_scanner_db_scan_cron_event($args = null)
{
    @set_time_limit(0);
    require_once('qtrDbSnapshot.php');
    require_once('qtrDatabaseScanner.php');

    $logger   = new CQtrLogger();
    $snapshot = load_db_snapshot($logger);
    if (!$snapshot) {
        $logger->Error('DB scan: snapshot not found, aborting');
        return;
    }

    // CHECK: Is another scan already running (file or DB phase)?
    // Uses timestamp transient set by mark_scan_started() with auto-expiry TTL.
    if (is_scan_in_progress()) {
        $last_start = get_transient(QTR_SCAN_PROGRESS_TRANSIENT);
        $elapsed = time() - $last_start;
        $logger->Debug("DB scan already in progress (started $elapsed seconds ago). Skipping this cron cycle.");
        return;  // Skip this cron execution - will retry in 5 minutes
    }

    // MARK: Start of new scan cycle
    // Sets transient lock with TTL > QTR_SCAN_CRON_TIMEOUT to persist across multiple cron firings.
    // Lock remains active through DB scan phase and is cleared when DB scan completes.
    mark_scan_started();
    $logger->Debug("Starting new scan cycle - timestamp lock set");

    $settings = QutteraSettings::LoadFromOptions();
    if (!$settings) {
        $settings = new QutteraSettings();
    }

    $scanner = new CQtrDatabaseScanner($settings->IsHeuristicMode());
    if (!$scanner->Initialize()) {
        $logger->Error('DB scan: failed to initialize pattern database');
        return;
    }
    
    // UPDATE: Ensure scan state is set to database_scanning
    $stats = new CQtrStats();
    $stats->State('database_scanning');

    $logger->Info(sprintf(
        'DB scan started. Phase: %s, table: %s, offset: %d',
        $snapshot->GetPhase(),
        $snapshot->CurrentTable() ?? 'n/a',
        $snapshot->GetOffset()
    ));

    $stime = time();
    $etime = $stime + QTR_SCAN_CRON_TIMEOUT;

    while (time() < $etime) {
        $more = $scanner->ScanBatch($snapshot);
        store_db_snapshot($snapshot, $logger); // checkpoint after every batch

        if (!$more) {
            $scanner->Finalize();
            clean_db_scan_hook();
            clear_scan_progress();
            delete_option(QTR_DB_SCAN_CRON_ARGS);
            $logger->Info('DB scan + transients scan completed successfully');
            
            // UPDATE: Set scan state to complete
            $stats = new CQtrStats();
            $stats->State('complete');
            
            return;
        }
    }

    // Time window exhausted — cron will fire again in 5 minutes
    $next_db_scan = wp_next_scheduled('qtr_db_scan_cron_hook');
    if ($next_db_scan) {
        $next_time_readable = date('Y-m-d H:i:s', $next_db_scan);
        $seconds_until = $next_db_scan - time();
        $logger->Info(sprintf('Time window exhausted, forcing next cron cycle to continue database scan. Next cycle scheduled in %d seconds at %s', $seconds_until, $next_time_readable));
        force_next_cron_job();
    } else {
        $logger->Info('DB scan: cron hook gone (scan was stopped externally)');
    }

    clear_scan_progress();
    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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access(); 
        if( self::IsInternalScanRunning() ){
	        echo "yes";
	    }else{
            echo "no";
        }
        exit();
    }

    
    public static function IsExternalScanNowRunning()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        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();
        // Don't populate entire filesystem here - it blocks the AJAX response!
        // Instead, add just the root directory to let cron job populate incrementally
        $fs->Push(ABSPATH);

        $logger->Info(sprintf("Snapshot initialized with root path"));
        /*
         * 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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        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();
        // Don't populate entire filesystem here - it blocks the AJAX response!
        // Instead, add just the root directory to let cron job populate incrementally
        $fs->Push(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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        $logger = new CQtrLogger();
        $logger->Info("Handling request to terminate internal scan");
        $logger->Info("Remove cron job from scheduler");

        clean_internal_scan_hook();

        clean_heur_internal_scan_hook();

        // Also stop the DB scan phase if it was chained from a file scan
        clean_db_scan_hook();
        delete_option(QTR_DB_SCAN_CRON_ARGS);
        
        // UPDATE: Set scan state to stopped
        $stats = new CQtrStats();
        $stats->State('stopped');

        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();
    }

    private static function IsInternalScanRunning()
    {
        // No need to verify access - this is an internal helper
        // All callers (public methods) already verify access
        /**
         * Frontend verification: Check if scan is actively running
         * Returns TRUE if any phase of the scan is in progress:
         *   - File scan phase (qtr_internal_scan_cron_hook)
         *   - Database scan phase (qtr_db_scan_cron_hook)
         *   - Active transient lock (scan running within timeout window)
         * 
         * This covers both normal and heuristic scanning modes.
         */
        
        // Check 1: Is scan actively running (transient lock exists)?
        // Transient is set when file scan starts and persists through DB scan phase.
        // TTL ensures self-healing if scan crashes (transient auto-expires).
        if (is_scan_in_progress()) {
            return true;
        }

        // Check 2: Is file scan phase scheduled?
        // Indicates filesystem snapshot waiting to be processed.
        if (wp_next_scheduled('qtr_internal_scan_cron_hook')) {
            return true;
        }
        
        // Check 3: Is database scan phase scheduled?
        // File scan completed and chained to DB scan (new phase).
        // This is critical for accurate frontend status during multi-phase scan.
        if (wp_next_scheduled('qtr_db_scan_cron_hook')) {
            return true;
        }
                
        // No active scan or scheduled phases
        return false;
    }

    public static function GetLogLines()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        $index  = 0;
        $logger = new CQtrLogger();
        $logger->Clean();        
        exit();
    }


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

    private static function _SeverityLabel( $severity ) {
        $sev = strtolower( $severity );
        if ( strpos( $sev, 'mal' ) !== false ) {
            return 'Malicious activity detected, immediate action recommended';
        }
        if ( strpos( $sev, 'pot' ) !== false ) {
            return 'Unusual behavior detected';
        }
        if ( strpos( $sev, 'susp' ) !== false ) {
            return 'Suspicious patterns identified, investigation recommended';
        }
        return $severity;
    }

    public static function ScannerReport()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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";
            $severity_label = self::_SeverityLabel( $entry["SEVERITY"] );

            // Handle both FILE and DATABASE entry types
            if(isset($entry["DATABASE"])) {
                // Database detection entry
                $body .= "DATABASE:     " . $entry["DATABASE"] . "\r\n";
                $body .= "TABLE:        " . $entry["TABLE"] . "\r\n";
                $body .= "ROW:          " . $entry["ROW"] . "\r\n";
                $body .= "SEVERITY:     " . $severity_label . "\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";
            } else {
                // File detection entry (original behavior)
                $body .= "FILE:         " . $entry["FILE"] . "\r\n";
                $body .= "FILE_MD5:     " . $entry["FILE_MD5"] . "\r\n";
                $body .= "SEVERITY:     " . $severity_label . "\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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        $logger = new CQtrLogger();
        $report = new CQtrReport();
        $output = $report->GetDetectedThreats();

        // Log breakdown to help diagnose visibility issues
        $file_count = 0;
        $db_count   = 0;
        foreach ( $output as $entry ) {
            if ( isset($entry['DATABASE']) ) { $db_count++; } else { $file_count++; }
        }
        $logger->Info(sprintf(
            'GetDetectedThreatsReport: total=%d (file=%d, db=%d)',
            count($output), $file_count, $db_count
        ));

        // json_encode silently returns false when any value contains invalid UTF-8.
        // Use JSON_INVALID_UTF8_SUBSTITUTE (PHP 7.2+) to replace bad bytes with
        // the Unicode replacement character instead of failing entirely.
        $json = json_encode($output, JSON_INVALID_UTF8_SUBSTITUTE);
        if ( $json === false ) {
            $logger->Error(sprintf(
                'GetDetectedThreatsReport: json_encode failed (%s), returning empty array',
                json_last_error_msg()
            ));
            $json = '[]';
        }

        echo $json;
        exit();
    }

    public static function ShowFile()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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
             * Handle both FILE and DATABASE entry types
             */
            if(isset($threat["DATABASE"])) {
                // Database detection - check ignore list by THREAT_SIG
                if( $ignore_list->Get( "", $threat["THREAT_SIG"] ) )
                {
                    array_push($output,$threat);
                }
            } else if(isset($threat["FILE_MD5"])) {
                // File detection - check ignore list by FILE_MD5 + THREAT_SIG
                if( $ignore_list->Get( $threat["FILE_MD5"], $threat["THREAT_SIG"] ) )
                {
                    array_push($output,$threat);
                }
            }
        }

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


    public static function IgnoreThreat()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        $ignore_list = new CQtrIgnoreList();
        $ignore_list->Clean();
        echo json_encode("Operation succeeded");
        exit();
    }


    public static function AcknowledgeAlarm()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        $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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        $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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        self::__can_access();
        $white_list = new CQtrThreatsWhiteList();
        $white_list->Clean();
        echo json_encode("Operation succeeded");
        exit();
    }


    public static function WhiteListFile()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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()
    {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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() {
        $timer = new CQtrAjaxTimerGuard(__FUNCTION__);
        
        // 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'),
        ));
    }

}

?>
