<?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');

define( 'QTR_SCAN_CRON_ARGS', 'qtr_scan_cron_args');

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

    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       cron job callback procedure
 * @param[in]   $args - list of input arguments 
 * @return      nothing
 */
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;
    }
    $logger->Info(sprintf("Snapshot %d elements", $snapshot->FilesCount()));
    $stime = time();
    $etime = $stime + QTR_SCAN_CRON_TIMEOUT;
    $scanner = new CQtrFilesScanner(FALSE/*not-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_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");
            /*
             * 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       cron job callback procedure
 * @param[in]   $args - list of input arguments 
 * @return      nothing
 */
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;
    $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
{

    private static function __can_access(){
        $nonce = $_REQUEST['_wpnonce'];
        if (!wp_verify_nonce( $nonce, 'quttera' ) ) {
            wp_die(__('You do not have sufficient permissions to access this page.') );
        }
        if(!current_user_can('manage_options')){
            wp_die(__('You do not have sufficient permissions to access this page.') );
        }
    }


    public static function RunExternalScan()
    {
        self::__can_access();        
        //check_ajax_referer( 'qtr_wm_scanner-scan' );
        $this_url = trim($_POST['_this']);        /* domain name of this server */
        $qtr_url  = 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;
            }
        }
        
        $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 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();

        $scanner = new CQtrFilesScanner(FALSE/*non-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_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();
    }


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

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


    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();        
        #echo __FUNCTION__ . " called\n";
        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.'));         
        }

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

        $file   = "";         
        foreach ( $dump  as $entry){
            if( strcasecmp($entry["FILE_MD5"], $file_hash)){
                $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);
        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 = $_POST["FILE_MD5"];
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = $_POST["THREAT_SIG"];
        }

        $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 = $_POST["FILE_MD5"];
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = $_POST["THREAT_SIG"];
        }

        $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 WhiteListThreat()
    {
        self::__can_access();
        $file   = "";
        $threat = "";

        if( isset( $_POST["FILE_MD5"] ) ){
            $file = $_POST["FILE_MD5"];
        }

        if( isset( $_POST["THREAT_SIG"] ) ){
            $threat = $_POST["THREAT_SIG"];
        }

        $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 = trim($_POST["FILE_MD5"]);
        }
        if( isset($_POST["FILE"]) ){
            $file_path = trim( $_POST["FILE"] );
        }

        if( $file_path == NULL && $file_sig == NULL ){
            echo json_encode("provided invalid input");
            exit();
        }

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

}

?>
