Current File : /home/n742ef5/royalanteam.com/wp-content/plugins/security-malware-firewall/inc/spbc-scanner.php
<?php

use CleantalkSP\Common\Scanner\SignaturesAnalyser\Structures\Verdict;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\API as SpbcAPI;
use CleantalkSP\SpbctWP\Helpers\CSV;
use CleantalkSP\SpbctWP\Scanner\CureLog\CureLog;
use CleantalkSP\SpbctWP\Scanner\FrontendScan;
use CleantalkSP\SpbctWP\Scanner\Stages\CureStage;
use CleantalkSP\SpbctWP\Scanner\Surface;
use CleantalkSP\Variables\Post;
use CleantalkSP\SpbctWP\Scanner\Links;
use CleantalkSP\SpbctWP\Scanner;
use CleantalkSP\SpbctWP\Helpers\HTTP;
use CleantalkSP\Common\Helpers\Arr;
use CleantalkSP\SpbctWP\DTO;
use CleantalkSP\Fpdf\Pdf;
use CleantalkSP\SpbctWP\Scanner\ScanningLog\ScanningLogFacade;
use CleantalkSP\SpbctWP\Scanner\Services\SendFileToCloudService;

/**
 * Cron wrapper function for launchBackground
 *
 * @return bool|string|string[]|void
 */
function spbc_scanner__launch()
{
    $result = \CleantalkSP\SpbctWP\Scanner\ScannerQueue::launchBackground();

    if (\CleantalkSP\SpbctWP\RemoteCalls::check()) {
        $result = empty($result['error'])
            ? 'OK'
            : 'FAIL ' . die(json_encode($result));
    }

    return $result;
}

/**
 * Cron wrapper function for controllerBackground
 *
 * @param null $transaction_id
 * @param null $stage
 * @param null $offset
 * @param null $amount
 *
 * @return bool|string|string[]
 */
function spbc_scanner__controller($transaction_id = null, $stage = null, $offset = null, $amount = null)
{
    //cron task provide a single parameter
    if (isset($transaction_id) && is_array($transaction_id)) {
        $stage = isset($transaction_id['stage']) ? $transaction_id['stage'] : null;
        $offset = isset($transaction_id['offset']) ? $transaction_id['offset'] : null;
        $amount = isset($transaction_id['amount']) ? $transaction_id['amount'] : null;
        $transaction_id = isset($transaction_id['transaction_id']) ? $transaction_id['transaction_id'] : null;
    }

    $result = \CleantalkSP\SpbctWP\Scanner\ScannerQueue::controllerBackground($transaction_id, $stage, $offset, $amount);

    if (\CleantalkSP\SpbctWP\RemoteCalls::check()) {
        $result = empty($result['error'])
            ? 'OK'
            : 'FAIL ' . die(json_encode($result));
    }

    return $result;
}

/**
 * /**
 * For debug purpose
 * Clear table from results
 *
 * @param $direct_call
 *
 * @return array|void
 */
function spbc_scanner_clear($direct_call = false)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    global $spbc;

    $spbc->plugins = array();
    $spbc->save('plugins');

    $spbc->themes = array();
    $spbc->save('themes');

    $spbc->data['scanner'] = array(
        'last_wp_version' => null,
        'cron'            => array(
            'state'         => 'get_hashes',
            'total_scanned' => 0,
            'offset'        => 0,
        ),
    );
    $spbc->save('data');

    $out = [
        'deleted_files_entries'    => Scanner\Controller::resetCheckResult(),
        'deleted_frontend_entries' => Scanner\Frontend::resetCheckResult(),
        'deleted_links'            => Scanner\Links::resetCheckResult(),
    ];

    if ($direct_call) {
        return $out;
    }

    wp_send_json($out);
}

/**
 * Get count of domains, use in domains accordion
 * @return int
 */
function spbc_scanner_links_count_found__domains()
{
    global $wpdb;
    $count = $wpdb->get_results(
        'SELECT COUNT(DISTINCT domain)
				FROM ' . SPBC_TBL_SCAN_LINKS . ';',
        OBJECT_K
    );

    return $count ? key($count) : 0;
}

/**
 * Get scanned domains, use in domains accordion
 * @param int $offset
 * @param int $amount
 * @param string $order
 * @param string $by
 * @param bool $get_array
 * @return array|object|null
 */
function spbc_scanner_links_get_scanned__domains($offset = 0, $amount = 20, $order = null, $by = null, $get_array = false)
{
    global $wpdb;
    $offset = intval($offset);
    $amount = intval($amount);
    $data   = $wpdb->get_results(
        'SELECT domain, page_url, COUNT(domain) as link_count
				FROM ' . SPBC_TBL_SCAN_LINKS . ' 
			GROUP BY domain
			' . ($order && $by ? "ORDER BY $by $order" : '') . '
			LIMIT ' . $offset . ',' . $amount . ';',
        $get_array === true ? ARRAY_A : OBJECT
    );

    return $data;
}

/**
 * Remove file from the database
 * @param int|string $file_id
 * @return bool
 */
function spbc_scanner_file_remove_from_log($file_id)
{
    global $wpdb;

    return $wpdb->delete(SPBC_TBL_SCAN_FILES, array('fast_hash' => $file_id));
}

/**
 * Send file to Cleantalk Cloud
 * @param bool $direct_call
 * @param int|string $file_id
 * @param bool $do_rescan
 * @param string $handler
 * @return array
 * @todo: check if $do_rescan is needed
 */
function spbc_scanner_file_send($direct_call = false, $file_id = null, $do_rescan = true, $handler = SendFileToCloudService::class)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
        $file_id = preg_match('@[a-zA-Z0-9]{32}@', Post::getString('file_id')) ? Post::getString('file_id') : null;
    }

    $output = $handler::sendFile($file_id, $do_rescan);

    if ( !$direct_call ) {
        wp_send_json($output);
    }

    return $output;
}


/**
 * Do rescan a single file with heuristic and signatures.
 * @param $file_path
 * @param $full_hash
 * @param $root_path
 * @return array[]|string[]
 * <ul>
 * <li>array('heuristic_result' => Verdict,
 * <br>'signatures_result' => Verdict,
 * <br>'merged_result' => array(
 * <br>&nbsp&nbsp&nbsp&nbsp 'status' => '',
 * <br>&nbsp&nbsp&nbsp&nbsp 'severity' => '',
 * <br>&nbsp&nbsp&nbsp&nbsp 'weak_spots' => '')) on success
 * </li>
 * <li>array('error' => 'error_text') on failure.</li>
 * </ul>
 */
function spbc_scanner_rescan_single_file($file_path, $full_hash, $root_path)
{
    global $wpdb;

    $out = array(
        'heuristic_result' => array(),
        'signatures_result' => array(),
        'merged_result' => array(),
    );

    //Heuristic
    $heuristic_scanner = new \CleantalkSP\Common\Scanner\HeuristicAnalyser\Controller();
    $file_to_check = new Scanner\FileInfoExtended(array('path' => $file_path));
    $result_heur = $heuristic_scanner->scanFile($file_to_check, $root_path);

    if ($result_heur->status === 'ERROR') {
        return array('error' => $result_heur->error_msg);
    }

    // Signature
    $signatures  = $wpdb->get_results('SELECT * FROM ' . SPBC_TBL_SCAN_SIGNATURES, ARRAY_A);
    $signatures_scanner = new \CleantalkSP\Common\Scanner\SignaturesAnalyser\Controller();
    $file_to_check = new \CleantalkSP\Common\Scanner\SignaturesAnalyser\Structures\FileInfo(
        $file_path,
        $full_hash
    );
    $result_sign = $signatures_scanner->scanFile($file_to_check, $root_path, $signatures);

    if ($result_sign->status === 'ERROR') {
        return array('error' => $result_sign->error_msg);
    }

    $out['heuristic_result'] = $result_heur;
    $out['signature_result'] = $result_sign;

    $merged_result = Arr::mergeWithSavingNumericKeysRecursive((array)$result_heur, (array)$result_sign);

    //merge weak-spots
    if ( isset($merged_result['weak_spots']) &&
        is_array($merged_result['weak_spots']) &&
        isset($merged_result['weak_spots'][0]) &&
        count($merged_result['weak_spots']) > 1 ) {
        unset($merged_result['weak_spots'][0]);
    }

    //merge status and severities
    if ($result_sign->status !== 'OK') {
        //signatures verdict is prior
        $merged_result['status'] = $result_sign->status;
        $merged_result['severity'] = $result_sign->severity;
    } elseif ($result_heur->status  !== 'OK') {
        //if no signatures found - check heuristic verict
        $merged_result['status'] = $result_heur->status;
        $merged_result['severity'] = $result_heur->severity;
    }

    $out['merged_result'] = $merged_result;

    return $out;
}

/**
 * Delete file from the database handler.
 * @param int|string $file_id
 * @return array
 */
function spbc_scanner_file_delete_handler($file_id)
{
    global $wpdb;

    if (!$file_id) {
        return array('error' => 'WRONG_FILE_ID');
    }

    $root_path = spbc_get_root_path();
    $response_content_ok = false;
    $response_content_admin_ok = false;

    // Getting file info.
    $sql = 'SELECT * FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = %s LIMIT 1';
    $sql_result = $wpdb->get_results($wpdb->prepare($sql, $file_id), ARRAY_A);
    $file_info  = $sql_result[0];

    if (empty($file_info)) {
        return array('error' => 'FILE_NOT_FOUND');
    }

    $file_path = $file_info['status'] == 'QUARANTINED' ? $file_info['q_path'] : $root_path . $file_info['path'];

    if (!file_exists($file_path)) {
        return array('error' => 'FILE_NOT_EXISTS');
    }

    if (!is_writable($file_path)) {
        return array('error' => 'FILE_NOT_WRITABLE');
    }

    $is_file_required_result = spbc_is_file_required_in_php_ini($file_path);
    if ($is_file_required_result !== false) {
        return $is_file_required_result === null
            ? array('error' => 'PHP_INI_REQUIREMENTS_CHECK_FAIL')
            : array('error' => 'FILE_IS_REQUIRED_IN_PHP_INI');
    }

    // Getting file and prechecking website
    $remembered_file_content = file_get_contents($file_path);
    $precheck_public = wp_remote_get(get_option('home'));
    $precheck_admin = wp_remote_get(get_option('home') . '/wp-admin');

    $result = unlink($file_path);

    if (!$result) {
        return array('error' => 'FILE_COULDNT_DELETE');
    }

    // if not work before and after unlink - it's ok, if both work - it's ok too and much better
    $postcheck_public = wp_remote_get(get_option('home'));
    if ((is_wp_error($postcheck_public) && is_wp_error($precheck_public)) ||
        (!is_wp_error($postcheck_public) && !is_wp_error($precheck_public) &&
        wp_remote_retrieve_response_code($postcheck_public) === wp_remote_retrieve_response_code($precheck_public))
    ) {
        $response_content_ok = true;
    }

    // if not work before and after unlink - it's ok, if both work - it's ok too and much better
    $postcheck_admin = wp_remote_get(get_option('home') . '/wp-admin');
    if ((is_wp_error($postcheck_admin) && is_wp_error($precheck_admin)) ||
        (!is_wp_error($postcheck_admin) && !is_wp_error($precheck_admin) &&
        wp_remote_retrieve_response_code($postcheck_admin) === wp_remote_retrieve_response_code($precheck_admin))
    ) {
        $response_content_admin_ok = true;
    }

    // general check to revert if something went wrong
    if (!$response_content_admin_ok || !$response_content_ok) {
        $result = file_put_contents($file_path, $remembered_file_content);
        $output = array('error' => 'WEBSITE_RESPONSE_BAD');
        $output['error'] .= $result === false ? ' REVERT_FAILED' : ' REVERT_OK';
        return $output;
    }

    // Deleting row from DB
    $sql = 'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = %s';
    $result = $wpdb->query($wpdb->prepare($sql, $file_id));
    if ($result !== false) {
        $output = array('success' => true);
    } else {
        $output = array('error' => 'DB_COULDNT_DELETE_ROW');
    }

    unset($remembered_file_content);

    return $output;
}

/**
 * Delete file from the database.
 * @param bool $direct_call
 * @param int|string $file_id
 * @return non-empty-array<array-key, mixed>|true
 */
function spbc_scanner_file_delete($direct_call = false, $file_id = null)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $time_start = microtime(true);

    $file_id = $direct_call ? $file_id : Post::getString('file_id');
    $output = spbc_scanner_file_delete_handler($file_id);

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
        return true;
    }
}

/**
 * Cheсk if the filepath is required in php.ini settings. It could be set in "auto_prepend_file" or in "auto_append_file" keys strings.
 * @param $file_path
 * @return bool|null True if files is required, false otherwise. Null on any code fail.
 */
function spbc_is_file_required_in_php_ini($file_path)
{
    try {
        $ini_auto_prepend_file_req[] = @ini_get('auto_prepend_file');
        $ini_auto_prepend_file_req[] = @ini_get('auto_append_file');
        if (!empty($file_path) && is_string($file_path)) {
            foreach ($ini_auto_prepend_file_req as $required_string) {
                if (is_string($required_string)) {
                    if (strpos($required_string, $file_path) !== false ||
                        (!empty(basename($file_path)) && strpos($required_string, basename($file_path)) !== false)) {
                        return true;
                    }
                }
            }
        }
    } catch (\Exception $_e) {
        return null;
    }
    return false;
}

/**
 * Approve file
 * @param bool $direct_call
 * @param int|string $file_id
 * @return void|array
 */
function spbc_scanner_file_approve($direct_call = false, $file_id = null)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    global $wpdb;

    $time_start = microtime(true);

    $root_path = spbc_get_root_path();
    $file_id   = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    if ($file_id) {
        // Getting file info.
        $sql        = $wpdb->prepare('SELECT path, status, severity, pscan_status, pscan_processing_status, pscan_balls, pscan_file_id
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = $sql_result[0];

        if ( ! empty($file_info)) {
            if (file_exists($root_path . $file_info['path'])) {
                if (is_readable($root_path . $file_info['path'])) {
                    // Getting file && API call
                    $previous = json_encode(array(
                        'status'   => $file_info['status'],
                        'severity' => $file_info['severity'],
                        'pscan_status' => $file_info['pscan_status'],
                        'pscan_processing_status' => $file_info['pscan_processing_status'],
                        'pscan_balls' => $file_info['pscan_balls'],
                        'pscan_file_id' => $file_info['pscan_file_id'],
                    ));

                    // Updating all other statuses
                    $wpdb->update(
                        SPBC_TBL_SCAN_FILES,
                        array(
                            'status'         => 'APPROVED_BY_USER',
                            'previous_state' => $previous,
                            'pscan_pending_queue' => null,
                            'pscan_status' => null,
                            'pscan_processing_status' => null,
                            'pscan_balls' => null,
                            'pscan_file_id' => null,
                        ),
                        array('fast_hash' => $file_id),
                        array('%s', '%s', '%s'),
                        array('%s')
                    );

                    // Set severity to NULL
                    // Using strait query because WPDB doesn't support NULL values
                    $sql        = 'UPDATE ' . SPBC_TBL_SCAN_FILES . '
                        SET severity = NULL
                        WHERE fast_hash = "' . $file_id . '"';
                    $sql_result = $wpdb->query($sql, ARRAY_A);

                    if ($sql_result !== false) {
                        $output = array('success' => true);
                    } else {
                        $output = array('error' => 'DB_COULDNT_UPDATE_ROW_APPROVE');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_READABLE');
                }
            } else {
                $output = array('error' => 'FILE_NOT_EXISTS');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
    }
}

/**
 * Checks analysis status of passed file(s)
 *
 * @param bool $direct_call Direct call flag. Show that the function was called directly from other function, not from AJAX
 * @param string|array $file_ids_input IDs of files to check the analysis status
 * @return array|true[]
 */
function spbc_scanner_pscan_check_analysis_status($direct_call = false, $file_ids_input = '')
{
    // Check ajax nonce
    if ( !$direct_call ) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
        $file_ids_input = Post::get('file_id', 'hash') ? (string)Post::getString('file_id') : '';
    }

    global $spbc, $wpdb;

    // Parse if there are more than 1 file
    $_file_ids_input = is_string($file_ids_input) ? explode(',', $file_ids_input) : $file_ids_input;

    // Prepare counters
    $counters = array(
        'queued' => 0,
        'updated' => 0,
        'failed' => 0,
        'skipped' => 0,
        'total' => count($_file_ids_input)
    );

    // Prepare out array
    $out = array('counters' => $counters);

    /*
     * Processing files start
     */
    foreach ( $_file_ids_input as $file_id ) {
        /*
         * Process a single file start
         */
        // Get file info.
        $file_info = spbc_scanner_get_file_by_id($file_id);

        // Validate file info
        try {
            $file_info = spbc_scanner_pscan_validate_file_info($file_info);
        } catch (\Exception $e) {
            $out['error_detail'][] = array(
                'error' => $e->getMessage(),
            );
            $counters['failed']++;
            continue;
        }

        try {
            $file_info = spbc_scanner_pscan_update_check_exclusions($file_info);
        } catch (\Exception $e) {
            switch ($e->getMessage()) {
                case 'skipped':
                    $counters['skipped']++;
                    break;
                case 'queued':
                    $counters['queued']++;
                    break;
            }
            continue;
        }


        // Perform API call
        $api_response = SpbcAPI::method__security_pscan_status(
            $spbc->settings['spbc_key'],
            $file_info['pscan_file_id']
        );


        // Validate API response
        try {
            $api_response = spbc_scanner_validate_pscan_status_response($api_response);
        } catch ( Exception $exception ) {
            $out['error_detail'][] = array(
                'file_path' => $file_info['path'],
                'pscan_file_id' => $file_info['pscan_file_id'],
                'fast_hash' => $file_id,
                'error' => 'API response validation failed "' . $exception->getMessage() . "\"",
            );
            continue;
        }

        if ( $api_response['processing_status'] !== 'DONE' ) {
            /*
            * If file process is not finished, update data
            */
            $update_result = $wpdb->query(
                $wpdb->prepare(
                    'UPDATE ' . SPBC_TBL_SCAN_FILES
                    . ' SET pscan_pending_queue = 0, pscan_processing_status  = %s, pscan_estimated_execution_time  = %s'
                    . ' WHERE pscan_file_id = %s',
                    $api_response['processing_status'],
                    $api_response['estimated_execution_time'],
                    $file_info['pscan_file_id']
                )
            );
        } else {
            if ( $api_response['file_status'] === 'SAFE' ) {
                /*
                * Do something with SAFE files
                */
                // Prepare query for good files update
                $update_query = $wpdb->prepare(
                    'UPDATE ' . SPBC_TBL_SCAN_FILES
                    . ' SET '
                    . ' pscan_processing_status  = "DONE",'
                    . ' pscan_pending_queue = 0, '
                    . ' pscan_status  = "SAFE",'
                    . ' pscan_balls  = %s,'
                    . ' status = "APPROVED_BY_CLOUD",'
                    . ' pscan_estimated_execution_time = NULL'
                    . ' WHERE pscan_file_id = %s',
                    isset($api_response['file_balls']) ? $api_response['file_balls'] : '{SAFE:0}',
                    $file_info['pscan_file_id']
                );
            } else {
                /*
                * Do something with DANGEROUS files
                */
                // Prepare query for bad files update
                $update_query = $wpdb->prepare(
                    'UPDATE ' . SPBC_TBL_SCAN_FILES
                    . ' SET '
                    . ' pscan_processing_status  = "DONE",'
                    . ' pscan_pending_queue = 0, '
                    . ' pscan_status  = %s ,'
                    . ' severity  = "CRITICAL",'
                    . ' pscan_balls  = %s,'
                    . ' status  = "DENIED_BY_CLOUD",'
                    . ' pscan_estimated_execution_time = NULL'
                    . ' WHERE pscan_file_id = %s',
                    $api_response['file_status'],
                    isset($api_response['file_balls']) ? $api_response['file_balls'] : '{DANGEROUS:0}',
                    $file_info['pscan_file_id']
                );
            }
            // Run prepared query and keep update result
            $update_result = $wpdb->query($update_query);
        }

        if ( $update_result === false ) {
            // Collect errors
            $out['error_detail'][] = array(
                'file_path' => $file_info['path'],
                'pscan_file_id' => $file_info['pscan_file_id'],
                'fast_hash' => $file_id,
                'error' => 'COULDNT_UPDATE file status',
            );
        } else {
            // All is fine, inc updated counter
            $counters['updated']++;
        }
        /*
        * Process a single file end
        */
    }

    /*
    * Processing files end
    */

    // Process errors
    if ( !empty($out['error_detail']) ) {
        $out['error'] = 'Some files where not updated.';
    }

    // Fill counters
    $out['counters'] = $counters;

    // Shift cron task
    \CleantalkSP\SpbctWP\Cron::updateTask(
        'scanner_update_pscan_files_status',
        'spbc_scanner_update_pscan_files_status',
        SPBC_PSCAN_UPDATE_FILES_STATUS_PERIOD,
        time() + SPBC_PSCAN_UPDATE_FILES_STATUS_PERIOD
    );

    // Resend queued files if available
    if ( $counters['queued'] > 0 && $spbc->settings['spbc_scanner_user_can_force_pscan_update'] ) {
        spbc_scanner_resend_pscan_files();
    }

    if (!$direct_call) {
        wp_send_json_success($out);
    }

    return $out;
}

/**
 * Validate file info collected for pscan status updating process
 * @param $file_info array|false array of file info or false if we could not collect this
 * @return array origin array of file info
 * @throws Exception
 */
function spbc_scanner_pscan_validate_file_info($file_info)
{
    // SQL query result validation
    if ( $file_info !== false ) {
        // Validate path
        $file_path = !empty($file_info['path']) ? $file_info['path'] : false;
        if (!$file_path) {
            throw new Exception('can not get file path');
        }

        // Set pscan file id to be sure that file is correctly reached from db
        $pscan_file_id = !empty($file_info['pscan_file_id']) ? $file_info['pscan_file_id'] : false;
        if ( !$pscan_file_id ) {
            throw new Exception('can not get pscan_file_id');
        }
    } else {
        throw new Exception('can not get file info');
    }
    return $file_info;
}


/**
 * Check if status updater should skip this file
 * @param $file_info
 * @return array array of file info
 * @throws Exception message contains reason for skip the file
 */
function spbc_scanner_pscan_update_check_exclusions(array $file_info)
{
    global $spbc;
    //skip quarantined files
    if (isset($file_info['status']) && $file_info['status'] === 'QUARANTINED') {
        throw new Exception('skipped');
    }

    // skip not queued files
    $pscan_pending_queue = isset($file_info['pscan_pending_queue']) && $file_info['pscan_pending_queue'] == '1';
    if ($pscan_pending_queue) {
        throw new Exception('queued');
    }

    //skip maual analysis checked
    if (empty($file_info['pscan_processing_status'])) {
        throw new Exception('skipped');
    }

    return $file_info;
}

/**
 * @param array $response API Response
 * @param bool $await_estimated_data Do await estimated data set on undone files check
 * @return mixed API Response
 * @throws Exception if validation failed
 */
function spbc_scanner_validate_pscan_status_response($response)
{
    // Check if API error
    if ( !empty($response['error']) ) {
        throw new Exception('API error: "' . $response['error'] . "\"");
    }

    // Check if processing_status is set and correct
    if ( !isset($response['processing_status']) ) {
        throw new Exception('response provided no processing status');
    }

    // Set allowed statuses
    $allowed_statuses_array = array(
        'NEW',
        'DONE',
        'ERROR',
        'IN_SCANER',
        'NEW_CLOUD',
        'IN_CLOUD',
        'IN_SANDBOX',
        'NEW_SANDBOX',
        'UNKNOWN'
    );

    // Check allowed statuses
    if ( !in_array($response['processing_status'], $allowed_statuses_array) ) {
        throw new Exception('response provided unknown processing status "' . $response['processing_status'] . "\"");
    }

    // Check precessing status
    if ( $response['processing_status'] === 'DONE' && !isset($response['file_status']) ) {
        throw new Exception('process finished, but status is unset');
    }

    if ( $response['processing_status'] === 'DONE' ) {
        // Check file_status
        if ( !in_array($response['file_status'], array('DANGEROUS', 'SAFE')) ) {
            throw new Exception('process finished, but status is unknown: "' . $response['file_status'] . "\"");
        }
    }

    //estimated time validation
    if ( $response['processing_status'] !== 'DONE' ) {
        if ( ! isset($response['estimated_execution_time'])) {
            throw new Exception('response provided no estimated scan time');
        }
        //todo remove on business decision
        //if ( ! isset($response['number_of_files'])) {
        //  throw new Exception('response provided no number of estimated files');
        //}
        //if ( ! isset($response['number_of_files_scanned'])) {
        //  throw new Exception('response provided no number of already scanned files');
        //}
    }

    return $response;
}

/**
 * Approve files in bulk
 * @param array $ids
 * @return array
 */
function spbc_scanner_file_approve__bulk($ids = array())
{
    if ( ! $ids) {
        return array('error' => 'Noting to approve');
    }

    $out = array();

    foreach ($ids as $id) {
        $result = spbc_scanner_file_approve(true, $id);

        if ( ! empty($result['error'])) {
            $file_info             = spbc_scanner_get_file_by_id($id);
            $file_path             = isset($file_info['path']) ? $file_info['path'] : 'UNKNOWN_FILE';
            $out['error']          = 'Some files where not updated.';
            $out['error_detail'][] = array(
                'file_path' => $file_path,
                'error'     => $result['error'],
            );
        }
    }

    return $out;
}

/**
 * Disapprove files in bulk
 * @param array $ids
 * @return array
 */
function spbc_scanner_file_disapprove__bulk($ids = array())
{
    if ( ! $ids ) {
        return array('error' => 'Nothing to disapprove');
    }

    $out = array();

    foreach ($ids as $id) {
        $result = spbc_scanner_file_disapprove(true, $id);

        if ( ! empty($result['error']) ) {
            $file_info             = spbc_scanner_get_file_by_id($id);
            $file_path             = isset($file_info['path']) ? $file_info['path'] : 'UNKNOWN_FILE';
            $out['error']          = 'Some files where not updated.';
            $out['error_detail'][] = array(
                'file_path' => $file_path,
                'error'     => $result['error'],
            );
        }
    }

    return $out;
}

/**
 * Approve pages in bulk
 * @param string $action
 * @return array
 */
function spbc_scanner_page_approve_process__bulk($action)
{
    global $wpdb;

    $action = $action === 'approve' ? '1' : '0';

    $out = array();

    $sql = 'UPDATE ' . SPBC_TBL_SCAN_FRONTEND . ' SET approved = ' . $action;
    $sql = $wpdb->prepare($sql);
    $sql_result = $wpdb->query($sql);
    if ($sql_result === false) {
        $out['error'] = 'COULDNT_UPDATE_DB';
    }

    $ids = $wpdb->get_col("SELECT page_id FROM " . SPBC_TBL_SCAN_FRONTEND);
    foreach ($ids as $id) {
        $update_meta_result = update_post_meta($id, '_spbc_frontend__approved', (int)$action);
        if ($update_meta_result === false) {
            $out['error'] = 'COULDNT_UPDATE_POST_META';
        }
    }

    return $out;
}

/**
 * Send files for analysis in bulk
 * @param array $fast_hashes_list
 * @return array
 */
function spbc_scanner_file_send_for_analysis__bulk($fast_hashes_list = array())
{
    if ( ! $fast_hashes_list) {
        return array('error' => 'Record has no file id to send.');
    }

    global $wpdb;

    $sql_result = $wpdb->get_results(
        'SELECT fast_hash FROM ' . SPBC_TBL_SCAN_FILES . '
        WHERE last_sent IS NULL
        AND status NOT IN ("APPROVED_BY_USER","APPROVED_BY_CT","APPROVED_BY_CLOUD","DENIED_BY_CT")',
        ARRAY_A
    );

    $sql_result = array_map(
        function ($data) {
            return $data['fast_hash'];
        },
        $sql_result
    );

    $not_sent_files_intersection = array_values(array_intersect($fast_hashes_list, $sql_result));

    $out = array(
        'files_sent_counter' => 0
    );

    if ( ! empty($not_sent_files_intersection)) {
        foreach ($not_sent_files_intersection as $fast_hash) {
            $result = spbc_scanner_file_send(true, $fast_hash);

            if ( ! empty($result['error'])) {
                $file_info             = spbc_scanner_get_file_by_id($fast_hash);
                $file_path             = isset($file_info['path']) ? $file_info['path'] : 'UNKNOWN_FILE';
                $out['error']          = 'Some files where not updated.';
                $out['error_detail'][] = array(
                    'file_path' => $file_path,
                    'error'     => $result['error'],
                );
            }

            $out['files_sent_counter']++;
        }
    } else {
        $out['error'] = __('All the available files have been already sent.', 'security-malware-firewall');
    }

    return $out;
}

/**
 * Get SQL *WHERE* suffix for SELECT query depends on files category.
 * @param string $category Category of files category which needs to be searched for
 * @return string SQL *WHERE* suffix.
 */
function spbc_get_sql_where_addiction_for_table_of_category($category)
{
    global $spbc;
    switch ($category) {
        case 'critical':
            $res = ' WHERE status IN ("DENIED_BY_CLOUD", "DENIED_BY_CT")
            OR (
                severity = "CRITICAL"
                AND (
                    status <> "QUARANTINED" AND 
                    status <> "APPROVED_BY_USER" AND 
                    status <> "APPROVED_BY_CT" AND
                    status <> "OK"
                    )
                AND (
                    last_sent IS NULL OR 
                    pscan_status = "DANGEROUS"
                )
            )';
            break;
        case 'suspicious':
            $res = ' WHERE severity <> "CRITICAL" AND
                        last_sent IS NULL AND
                        (status = "MODIFIED" AND source_type IS NOT NULL) 
                        OR (status = "INFECTED" AND severity = "SUSPICIOUS" AND last_sent IS NULL)';
            break;
        case 'approved':
            $res = ' WHERE status = "APPROVED_BY_USER" AND source_type IS NULL';
            break;
        case 'approved_by_cloud':
            $res = ' WHERE ( status = "APPROVED_BY_CT" OR status = "APPROVED_BY_CLOUD") AND source_type IS NULL';
            break;
        case 'analysis_log':
            $res = ' WHERE last_sent IS NOT NULL';
            break;
        case 'unknown':
            $res = ' WHERE status NOT IN ("APPROVED_BY_USER","APPROVED_BY_CT","APPROVED_BY_CLOUD","DENIED_BY_CT","ERROR") AND
						    detected_at >= ' . (time() - $spbc->settings['scanner__list_unknown__older_than'] * 86400) . ' AND
						    source IS NULL AND
						    source_type IS NULL AND
						    size > 0 AND
		                    path NOT LIKE "%wp-content%themes%" AND
                            path NOT LIKE "%wp-content%plugins%" AND
                            path NOT LIKE "%wp-content%cache%" AND
                            path NOT LIKE "%wp-config.php" AND
						    (severity IS NULL OR severity NOT IN ("CRITICAL", "SUSPICIOUS")) AND
						    last_sent IS NULL AND
						    fast_hash NOT IN (SELECT fast_hash FROM ' . SPBC_TBL_CURE_LOG . ' WHERE cure_status = 1)';
            break;
        case 'quarantined':
            $res = ' WHERE status = "QUARANTINED"';
            break;
        case 'frontend_malware':
            $res = ' WHERE approved IS NULL OR approved <> 1';
            break;
        case 'frontend_scan_results_approved':
            $res = ' WHERE approved = 1';
            break;
        case 'skipped':
            $res = ' WHERE status = "ERROR" AND error_msg IS NOT NULL AND error_msg NOT LIKE "%FILE_SIZE_ZERO%"';
            break;
        default:
            $res = '';
    }
    return $res;
}

/**
 * Get all files IDs of the category.
 * @param string $category Category of files category which needs to be searched for
 * @param bool $count Whether to return the count of files or the files themselves
 * @return array Array of IDs
 */
function spbc_scanner_get_files_by_category($category, $count = false)
{
    global $wpdb;

    $ids = array();

    // No need to select IDs for Frontend Malware Scanner
    if ( $category === 'frontend_malware' ) {
        return $ids;
    }

    $query = $count ? 'SELECT COUNT(fast_hash) from ' : 'SELECT fast_hash from ';
    $query .= SPBC_TBL_SCAN_FILES . spbc_get_sql_where_addiction_for_table_of_category($category);

    // suppress because data is static
    // @psalm-suppress WpdbUnsafeMethodsIssue
    $res = $wpdb->get_results($query);

    foreach ($res as $tmp) {
        $ids[] = isset($tmp->fast_hash) ? $tmp->fast_hash : null;
    }

    return $ids;
}

/**
 * Disapprove file
 * @param bool $direct_call
 * @param int|string $file_id
 * @return void|array
 */
function spbc_scanner_file_disapprove($direct_call = false, $file_id = null)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $time_start = microtime(true);

    global $wpdb;

    $root_path = spbc_get_root_path();
    $file_id   = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    if ($file_id) {
        // Getting file info.
        $sql        = $wpdb->prepare('SELECT path, full_hash, previous_state
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = $sql_result[0];

        if ( ! empty($file_info) ) {
            if (file_exists($root_path . $file_info['path'])) {
                if (is_readable($root_path . $file_info['path'])) {
                    // Getting file && API call

                    $previous = is_string($file_info['previous_state'])
                        ? json_decode($file_info['previous_state'], true)
                        : false;

                    if ( ! $previous ) {
                        // Placeholders for the approved by CT files
                        $previous['status'] = 'OK';
                        $previous['severity'] = null;
                        $previous['pscan_status'] = null;
                        $previous['pscan_processing_status'] = null;
                        $previous['pscan_balls'] = null;
                        $previous['pscan_file_id'] = null;
                    }

                    $sql_upd_result = $wpdb->update(
                        SPBC_TBL_SCAN_FILES,
                        array(
                            'status' => $previous['status'],
                            'severity' => $previous['severity'],
                            'pscan_status' => $previous['pscan_status'],
                            'pscan_processing_status' => $previous['pscan_processing_status'],
                            'pscan_balls' => $previous['pscan_balls'],
                            'pscan_file_id' => $previous['pscan_file_id'],
                        ),
                        array('fast_hash' => $file_id),
                        array('%s', '%s', '%s', '%s', '%s', '%d'),
                        array('%s')
                    );

                    if ($sql_upd_result !== false) {
                        $output = array('success' => true);
                    } else {
                        $output = array('error' => 'DB_COULDNT_UPDATE_ROW_APPROVE');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_READABLE');
                }
            } else {
                $output = array('error' => 'FILE_NOT_EXISTS');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
    }
}

function spbc_scanner_page_view($direct_call = false, $page_url = false)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $time_start = microtime(true);

    global $spbc, $wpdb;

    $page_url = $direct_call
        ? $page_url
        : Post::get('page_url', null, 'url');

    $page_content = HTTP::getContentFromURL($page_url);

    if ( ! empty($page_content)) {
        // Getting signatures
        $check_list = array('redirects', 'dbd', 'signatures_js', 'signatures_html');
        if ($spbc->settings['scanner__frontend_analysis__csrf']) {
            $check_list[] = 'csrf';
        }

        $fe_scanner = new FrontendScan($check_list);

        $recheck_res = $fe_scanner->setHomeUrl(get_option('home'))
                                  ->setExceptUrls(CSV::parseNSV($spbc->settings['scanner__frontend_analysis__domains_exclusions']))
                                  ->setSignatures($wpdb->get_results('SELECT * FROM ' . SPBC_TBL_SCAN_SIGNATURES, ARRAY_A))
                                  ->setContent($page_content)
                                  ->check()
                                  ->getResult();

        if (count($recheck_res) === 0) {
            // If the malware not more present
            $page_id = $wpdb->get_var(
                $wpdb->prepare(
                    'SELECT page_id'
                    . ' FROM ' . SPBC_TBL_SCAN_FRONTEND
                    . ' WHERE url = %s',
                    $page_url
                )
            );
            delete_post_meta($page_id, '_spbc_frontend__last_checked');
            delete_post_meta($page_id, 'spbc_frontend__last_checked');
            $wpdb->query(
                $wpdb->prepare(
                    'DELETE'
                    . ' FROM ' . SPBC_TBL_SCAN_FRONTEND
                    . ' WHERE url = %s',
                    $page_url
                )
            );

            wp_send_json([
                'success' => false,
                'content' => esc_html__('The malware found earlier no longer present. The notice about the malware will be replaced from the results list.', 'security-malware-firewall'), // Content of the modal
                'file_path' => esc_html__('The malware no longer found', 'security-malware-firewall') // Title of the modal
            ]);
        }

        $page_text = array();

        $page_url_sql = str_replace('.', '%', $page_url);

        // Getting file info.
        $sql_result = $wpdb->get_results(
            $wpdb->prepare(
                'SELECT weak_spots'
                . ' FROM ' . SPBC_TBL_SCAN_FRONTEND
                . ' WHERE url LIKE %s'
                . ' LIMIT 1',
                $page_url_sql
            ),
            ARRAY_A
        );

        $result = $sql_result[0];

        foreach (preg_split("/((\r?\n)|(\r\n?))/", $page_content) as $line) {
            $page_text[] = htmlspecialchars($line);
        }
        $output = array(
            'success'    => true,
            'file'       => $page_text,
            'file_path'  => $page_url,
            'difference' => null,
            'weak_spots' => $result['weak_spots']
        );
    } else {
        $output = array('error' => 'FILE_TEXT_EMPTY');
    }

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    }
    $red_line             = '<span style=\"background: rgb(200,80,80);\">';
    $red_line_end         = '</span>';
    $output['weak_spots'] = str_replace('__SPBCT_RED__', $red_line, $output['weak_spots']);
    $output['weak_spots'] = str_replace('__SPBCT_RED_END__', $red_line_end, $output['weak_spots']);

    wp_send_json($output);
}

/**
 * Handler to approve or disapprove a page.
 * @param string $action Could be 'approve' or 'disapprove'
 * @return true
 * @throws Exception
 */
function spbc_scanner_page_approve_process($action)
{
    global $wpdb;

    if (!in_array($action, array('approve', 'disapprove'))) {
        throw new \Exception('APPROVE_PROCESS_MISTYPE');
    }

    $action = $action === 'approve' ? '1' : '0';

    $page_url = Post::get('page_url', null, 'url');
    $page_id = Post::getInt('page_id');

    if (empty($page_url) || $page_id === 0 ) {
        throw new \Exception('PAGE_ID_OR_PAGE_URL_IS_EMPTY');
    }

    if (filter_var($page_url, FILTER_VALIDATE_URL)) {
        // Getting file info.
        $sql = 'UPDATE ' . SPBC_TBL_SCAN_FRONTEND . ' SET approved = ' . $action . ' WHERE url = %s';
        $sql = $wpdb->prepare($sql, $page_url);
        $sql_result = $wpdb->query($sql);
        if ($sql_result === false) {
            throw new \Exception('COULDNT_UPDATE_DB');
        }
        $update_meta_result = update_post_meta($page_id, '_spbc_frontend__approved', (int)$action);
        if (false === $update_meta_result) {
            throw new \Exception('COULDNT_UPDATE_POST_META');
        }
    }

    return true;
}

/**
 * View file
 * @param bool $direct_call
 * @param int|string $file_id
 * @return void|array
 */
function spbc_scanner_file_view($direct_call = false, $file_id = null)
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $time_start = microtime(true);

    global $wpdb;

    $root_path = spbc_get_root_path();
    $file_id   = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    // check if file exists, delete it from the list if not
    $check_file_exist_result = \CleantalkSP\SpbctWP\ListTable::spbcCheckFileExist($file_id);
    if (isset($check_file_exist_result['error'])) {
        $out = array(
            'error'    => $check_file_exist_result['error'],
        );
        wp_send_json($out);
    }

    if ($file_id) {
        // Getting file info.
        $sql = $wpdb->prepare(
            'SELECT *
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1',
            $file_id
        );
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = isset($sql_result[0]) ? $sql_result[0] : null;

        if ( ! empty($file_info)) {
            $file_path = $file_info['status'] == 'QUARANTINED' ? $file_info['q_path'] : $root_path . $file_info['path'];

            if (file_exists($file_path)) {
                if (is_readable($file_path)) {
                    // Getting file && API call
                    $file = file($file_path);

                    if ($file !== false && count($file)) {
                        $file_text = array();
                        for ($i = 0; isset($file[ $i ]); $i++) {
                            $file_text[ $i + 1 ] = htmlspecialchars($file[ $i ]);
                            $file_text[ $i + 1 ] = preg_replace("/[^\S]{4}/", "&nbsp;", $file_text[ $i + 1 ]);
                        }

                        if ( ! empty($file_text)) {
                            $output = array(
                                'success'    => true,
                                'file'       => $file_text,
                                'file_path'  => $file_path,
                                'difference' => $file_info['difference'],
                                'weak_spots' => $file_info['weak_spots']
                            );
                        } else {
                            $output = array('error' => 'FILE_TEXT_EMPTY');
                        }
                    } else {
                        $output = array('error' => 'FILE_EMPTY');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_READABLE');
                }
            } else {
                $output = array('error' => 'File not exists and will be removed from log through next scan.');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
    }
}

/**
 * Replace file
 * @param bool $direct_call
 * @param int|string $file_id
 * @param string $_platform
 * @return void|array
 */
function spbc_scanner_file_replace($direct_call = false, $file_id = null, $_platform = 'wordpress')
{
    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $time_start = microtime(true);

    global $wpdb;

    $root_path = spbc_get_root_path();

    $file_id = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    if ($file_id) {
        // Getting file info.
        $sql        = $wpdb->prepare('SELECT path, source_type, source, version, status, severity, source_type
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = $sql_result[0];

        if ( ! empty($file_info)) {
            if (file_exists($root_path . $file_info['path'])) {
                if (is_writable($root_path . $file_info['path'])) {
                    // Getting file && API call
                    $original_file = Scanner\Helper::getOriginalFile($file_info);

                    if ($original_file && !isset($original_file['error'])) {
                        $file_desc = fopen($root_path . $file_info['path'], 'w');
                        if ($file_desc && is_string($original_file)) {
                            $res_fwrite = fwrite($file_desc, $original_file);
                            if ($res_fwrite) {
                                fclose($file_desc);

                                $db_result = $wpdb->query(
                                    $wpdb->prepare(
                                        'DELETE FROM ' . SPBC_TBL_SCAN_FILES
                                        . ' WHERE fast_hash = %s;',
                                        $file_id
                                    )
                                );

                                if ($db_result) {
                                    $output = array('success' => true,);
                                } else {
                                    $output = array('error' => 'FILE_DB_DELETE_FAIL');
                                }
                            } else {
                                $output = array('error' => 'FILE_COULDNT_WRITE');
                            }
                        } else {
                            $output = array('error' => 'FILE_COULDNT_OPEN');
                        }
                    } else {
                        $output = array('error' => 'GET_FILE_FAILED');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_WRITABLE');
                }
            } else {
                $output = array('error' => 'FILE_NOT_EXISTS');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    $exec_time           = round(microtime(true) - $time_start);
    $output['exec_time'] = $exec_time;

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
    }
}

/**
 * Quarantine file
 * @param bool $direct_call
 * @param int|string $file_id
 * @return void|array
 */
function spbc_scanner_file_quarantine($direct_call = false, $file_id = null)
{
    global $wpdb;

    if ( ! $direct_call ) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $root_path = spbc_get_root_path();
    $file_id   = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    if ($file_id) {
        // Getting file info.
        $sql        = $wpdb->prepare('SELECT *
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = $sql_result[0];

        if ( ! empty($file_info)) {
            if (file_exists($root_path . $file_info['path'])) {
                if (is_writable($root_path . $file_info['path'])) {
                    $q_path = SPBC_PLUGIN_DIR . 'quarantine/'
                              . str_replace('/', '__', str_replace('\\', '__', $file_info['path'])) . '___'
                              . md5($file_info['path'] . rand(0, 99999999)) . '.punished';

                    $dir_name = SPBC_PLUGIN_DIR . 'quarantine/';
                    if ( ! is_dir($dir_name)) {
                        mkdir($dir_name);
                        file_put_contents($dir_name . 'index.php', '<?php');
                    }
                    if (copy($root_path . $file_info['path'], $q_path)) {
                        $result = $wpdb->update(
                            SPBC_TBL_SCAN_FILES,
                            array(
                                'status'         => 'QUARANTINED',
                                'q_path'         => $q_path,
                                //should be offset to use in date()
                                'q_time'         => current_time('timestamp'),
                                'previous_state' => json_encode(array(
                                    'status' => $file_info['status'],
                                )),
                            ),
                            array('full_hash' => $file_info['full_hash'], 'fast_hash' => $file_info['fast_hash']),
                            array('%s', '%s', '%d', '%s'),
                            array('%s', '%s')
                        );
                        if ($result !== false && $result > 0) {
                            if (unlink($root_path . $file_info['path'])) {
                                $response_content       = HTTP::getContentFromURL(get_option('home'));
                                $response_content_admin = HTTP::getContentFromURL(get_option('home') . '/wp-admin/');
                                if (
                                    isset(
                                        $response_content['error'],
                                        $response_content_admin['error']
                                    ) ||
                                    spbc_search_page_errors($response_content) ||
                                    spbc_search_page_errors($response_content_admin)
                                ) {
                                    $output          = array('error' => 'WEBSITE_RESPONSE_BAD');
                                    $result          = spbc_scanner_file_quarantine__restore(true, $file_info['fast_hash']);
                                    $output['error'] .= ! empty($result['error']) ? ' REVERT_FAILED ' . $result['error'] : ' REVERT_OK';
                                } else {
                                    $output = array('success' => true,);
                                }
                            } else {
                                $output = array('error' => 'DELETE_FAILED');
                            }
                        } else {
                            $output = array('error' => 'UPDATE_TABLE_FAILED');
                        }
                    } else {
                        $output = array('error' => 'COPY_FAILED');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_WRITABLE');
                }
            } else {
                $output = array('error' => 'FILE_NOT_EXISTS');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    if ($direct_call) {
        return spbc_humanize_output($output);
    } else {
        wp_send_json($output);
    }
}

/**
 * Restore file from quarantine
 * @param bool $direct_call
 * @param int|string $file_id
 * @return void|array
 */
function spbc_scanner_file_quarantine__restore($direct_call = false, $file_id = null)
{
    global $wpdb;

    if ( ! $direct_call) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $root_path = spbc_get_root_path();
    $file_id   = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    if ($file_id) {
        // Getting file info.
        $sql = $wpdb->prepare('SELECT *
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = %s
			LIMIT 1', $file_id);
        $sql_result = $wpdb->get_results($sql, ARRAY_A);
        $file_info  = $sql_result[0];

        if ( ! empty($file_info)) {
            if (
                !empty($file_info['q_path']) &&
                is_string($file_info['q_path']) &&
                file_exists($file_info['q_path'])
            ) {
                if (is_writable($file_info['q_path'])) {
                    if (copy($file_info['q_path'], $root_path . $file_info['path'])) {
                        $previous = json_decode($file_info['previous_state'], true);

                        $result = $wpdb->update(
                            SPBC_TBL_SCAN_FILES,
                            array(
                                'status' => $previous['status'],
                                'q_path' => null,
                                'q_time' => null,
                            ),
                            array('fast_hash' => $file_info['fast_hash']),
                            array('%s', '%s', '%d',),
                            array('%s')
                        );
                        if ($result !== false && $result > 0) {
                            if (unlink($file_info['q_path'])) {
                                $output = array('success' => true,);
                            } else {
                                $output = array('error' => 'DELETE_FAILED');
                            }
                        } else {
                            $output = array('error' => 'UPDATE_TABLE_FAILED');
                        }
                    } else {
                        $output = array('error' => 'COPY_FAILED');
                    }
                } else {
                    $output = array('error' => 'FILE_NOT_WRITABLE');
                }
            } else {
                $output = array('error' => 'FILE_NOT_EXISTS');
            }
        } else {
            $output = array('error' => 'FILE_NOT_FOUND');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    if ($direct_call) {
        return $output;
    } else {
        wp_send_json($output);
    }
}

/**
 * Download file handler
 */
function spbc_scanner_file_download_handler($file_id)
{
    global $wpdb;

    if (!$file_id) {
        return array('error' => 'WRONG_FILE_ID');
    }

    $sql = $wpdb->prepare('SELECT *
        FROM ' . SPBC_TBL_SCAN_FILES . '
        WHERE fast_hash = %s
        LIMIT 1', $file_id);
    $sql_result = $wpdb->get_results($sql, ARRAY_A);

    if (empty($sql_result) || !isset($sql_result[0])) {
        return array('error' => 'FILE_NOT_FOUND');
    }

    $file_info = $sql_result[0];

    if (empty($file_info) || !isset($file_info['q_path'])) {
        return array('error' => 'FILE_NOT_FOUND');
    }

    if (!file_exists($file_info['q_path'])) {
        return array('error' => 'FILE_NOT_EXISTS');
    }

    if (!is_readable($file_info['q_path'])) {
        return array('error' => 'FILE_NOT_READABLE');
    }

    // Getting file && API call
    $file_path = substr($file_info['q_path'], stripos($file_info['q_path'], 'wp-content'));

    $file_content = HTTP::getContentFromURL(get_home_url() . '/' . $file_path);

    if (!empty($file_content['error'])) {
        return array('error' => 'FILE_EMPTY');
    }

    return array(
        'file_name'    => preg_replace('/.*(\/|\\\\)(.*)/', '$2', $file_info['path']),
        'file_content' => $file_content,
    );
}

/**
 * Download file function
 */
function spbc_scanner_file_download($direct_call = false, $file_id = null)
{
    $file_id = $direct_call
        ? $file_id
        : Post::get('file_id', 'hash');

    $result = spbc_scanner_file_download_handler($file_id);

    if ($direct_call) {
        return $result;
    } else {
        wp_send_json($result);
    }
}

/**
 * Delete file from analysis log
 */
function spbc_scanner_analysis_log_delete_from_log($direct_call = false)
{
    // Check ajax nonce
    if ( ! $direct_call ) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    global $wpdb;

    $file_ids = Post::getArray('file_ids'); // validation is done in Validate::isHash method next line
    $file_ids_clean = [];

    if ( is_array($file_ids) ) {
        // Validate if the ID is hash (SQL-clear)
        $file_ids_clean = array_map(function ($_id) {
            if ( \CleantalkSP\Common\Validate::isHash($_id) ) {
                return $_id;
            }
        }, $file_ids);
    }

    $output = array('error' => false);

    if ( $file_ids_clean ) {
        $file_ids_string = '';
        foreach ( $file_ids_clean as $id ) {
            $file_ids_string .= '"' . $id . '",';
        }
        $query = "UPDATE " . SPBC_TBL_SCAN_FILES . " SET 
            last_sent = null,
            pscan_status = null,
            pscan_processing_status = null,
            pscan_pending_queue = null,
            pscan_balls = null,
            pscan_file_id = null 
            WHERE fast_hash IN (" . trim($file_ids_string, ',') . ")";
        // suppress because data is already prepared in Validate::isHash method
        // @psalm-suppress WpdbUnsafeMethodsIssue
        $updated_rows = $wpdb->query($query);

        if ( ! $updated_rows) {
            $output = array('error' => 'DB_ERROR');
        }
    } else {
        $output = array('error' => 'WRONG_FILE_ID');
    }

    if ( ! $direct_call ) {
        wp_send_json($output);
    }

    return $output;
}

/**
 * Replacing error codes by readable and translatable format.
 * We have to add new error descriptions here future.
 *
 * @param $output_array
 *
 * @return array
 */
function spbc_humanize_output($output_array)
{
    if (is_array($output_array) && array_key_exists('error', $output_array)) {
        $errors_codes = array(
            'WEBSITE_RESPONSE_BAD',
            'REVERT_OK'
        );
        $errors_texts = array(
            esc_html__('The requested action caused a website error.', 'security-malware-firewall'),
            // WEBSITE_RESPONSE_BAD
            esc_html__('The changes were reverted.', 'security-malware-firewall'),
            // REVERT_OK
        );
        foreach ($output_array as $key => $item) {
            $output_array[ $key ] = str_replace($errors_codes, $errors_texts, $item);
        }
    }

    return $output_array;
}

/**
 * Get file by ID
 * @param string $file_id
 * @return array|false
 */
function spbc_scanner_get_file_by_id($file_id)
{

    global $wpdb;

    $file_info = $wpdb->get_row(
        'SELECT *
			FROM ' . SPBC_TBL_SCAN_FILES . '
			WHERE fast_hash = "' . $file_id . '"
			LIMIT 1',
        ARRAY_A
    );

    return $file_info ?: false;
}

/**
 * Load more scan logs on the scan results page
 */
function spbc_scanner_load_more_scan_logs()
{
    $offset = Post::getInt('offset');

    $scan_logs = ScanningLogFacade::render(100, (int)$offset, true);

    wp_send_json_success($scan_logs);
}

/**
 * Save scanner logs to pdf
 *
 * @param $direct_call
 *
 * @return array|void
 */
function spbc_scanner_save_to_pdf($direct_call = false)
{
    if ( !$direct_call ) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    $pdf = new Pdf();

    $pdf->AliasNbPages();

    $pdf->AddPage();
    $pdf->drawScanCommonStatsTable();

    $pdf->Ln();
    $pdf->drawScanResultsOfScanType('heuristic_results');
    $pdf->drawScanResultsOfScanType('signature_results');

    $pdf->Ln();
    $pdf->drawFilesListByType('cure_log');

    $pdf->AddPage();
    $pdf->drawFilesListByType('critical_files');

    $pdf->AddPage();
    $pdf->drawFilesListByType('suspicious_files');

    $pdf->AddPage();
    $pdf->drawScanLogs();

    $pdf->Output();
}

/**
 * Get PDF file name
 * @param bool $direct_call
 * @return void|string
 */
function spbc_scanner_get_pdf_file_name($direct_call = false)
{

    if ( !$direct_call ) {
        spbc_check_ajax_referer('spbc_secret_nonce', 'security');
    }

    global $spbc;
    wp_send_json_success('spbct-mscan-'
        . preg_replace('/^http(s)?:\/\//', '', site_url())
        . '-'
        . date('M-d-Y', $spbc->data['scanner']['last_scan'])
        . '.pdf');
}

/**
 * AJAX handler for cure action.
 * @return void|string|WP_Error
 */
function spbc_file_cure_ajax_action()
{
    spbc_check_ajax_referer('spbc_secret_nonce', 'security');

    $file_fast_hash = isset($_POST['file_fast_hash']) ? esc_sql($_POST['file_fast_hash']) : null;

    $result = spbc_cure_file($file_fast_hash);

    if (is_wp_error($result)) {
        wp_send_json_error($result->get_error_message());
    }

    wp_send_json_success($result);
}

/**
 * AJAX handler for cure action.
 * @param string $file_fast_hash
 * @return string|WP_Error
 */
function spbc_cure_file($file_fast_hash)
{
    global $wpdb;

    if (is_null($file_fast_hash)) {
        return new WP_Error(
            '422',
            esc_html__('Error: File not found.', 'security-malware-firewall')
        );
    }

    $file_data = $wpdb->get_row(
        'SELECT * '
        . ' FROM ' . SPBC_TBL_SCAN_FILES
        . ' WHERE fast_hash="' . $file_fast_hash . '";',
        ARRAY_A
    );

    if (empty($file_data)) {
        return new WP_Error(
            '422',
            esc_html__('Error: File not found in table.', 'security-malware-firewall')
        );
    }

    // rescan file on a single run!
    $rescan_results = spbc_scanner_rescan_single_file($file_data['path'], md5_file(spbc_get_root_path() . $file_data['path']), spbc_get_root_path());
    $merged_result = $rescan_results['merged_result'];
    $verdict = isset($rescan_results['signature_result']) && $rescan_results['signature_result'] instanceof Verdict
        ? $rescan_results['signature_result']
        : new Verdict();

    $cure_log = new CureLog();
    if ( $verdict->status === 'OK') {
        $cure_log->deleteCureLogRecord($file_data['fast_hash']);
        // update file in the table
        $wpdb->update(
            SPBC_TBL_SCAN_FILES,
            array(
                'checked_signatures' => 1,
                'checked_heuristic'  => 1,
                'status'             => $file_data['status'] === 'MODIFIED' ? 'MODIFIED' : $merged_result['status'],
                'severity'           => $merged_result['severity'],
                'weak_spots'         => json_encode($merged_result['weak_spots']),
                'full_hash'          => md5_file(spbc_get_root_path() . $file_data['path']),
            ),
            array('fast_hash' => $file_data['fast_hash']),
            array('%s', '%s', '%s', '%s', '%s', '%s'),
            array('%s')
        );
        return esc_html__('No threats detected for current file statement.', 'security-malware-firewall');
    }

    $cure_stage = new CureStage(DB::getInstance());
    $cure_log_record = $cure_stage->processCure($file_data);

    $cure_log->logCureResult($cure_log_record);

    if ( !empty($cure_log_record->fail_reason) ) {
        return new WP_Error(
            '422',
            esc_html__('Error: ' . $cure_log_record->fail_reason, 'security-malware-firewall')
        );
    }

    return esc_html__('Success!', 'security-malware-firewall');
}

/**
 * Restore file from backup handler
 * @param string $id
 * @return array
 */
function spbc_restore_file_from_backup_ajax_action_handler($id)
{
    global $wpdb;

    if (is_null($id)) {
        return array('error' => esc_html__('Error: File not found.', 'security-malware-firewall'));
    }

    // Getting file path
    $file_path_sql_prepared = $wpdb->prepare(
        'SELECT path '
        . ' FROM ' . SPBC_TBL_SCAN_FILES
        . ' WHERE fast_hash=%s;',
        $id
    );
    $file_path = $wpdb->get_row($file_path_sql_prepared, ARRAY_A);

    if (is_null($file_path)) {
        return array('error' => esc_html__('Error: File path not found.', 'security-malware-firewall'));
    }

    $file_path = $file_path['path'];
    $full_file_path = ABSPATH . ltrim($file_path, '\\');

    // Getting backup path
    $sql_prepared = $wpdb->prepare(
        'SELECT back_path, backup_prev_results_state '
        . ' FROM ' . SPBC_TBL_BACKUPED_FILES
        . ' WHERE real_path="%s"'
        . ' ORDER BY backup_id DESC LIMIT 1;',
        $file_path
    );
    // prepare backup data
    $backup_data = $wpdb->get_results($sql_prepared, ARRAY_A);
    $backup_path = isset($backup_data[0]['back_path']) ? $backup_data[0]['back_path'] : null;
    // prepare previous state of the file
    $backup_prev_results_state = isset($backup_data[0]['backup_prev_results_state']) ? $backup_data[0]['backup_prev_results_state'] : null;
    $backup_prev_results_state = empty($backup_prev_results_state) || json_decode($backup_prev_results_state, true) === false
        ? null
        : json_decode($backup_prev_results_state, true);
    if (
        !isset(
            $backup_prev_results_state['weak_spots'],
            $backup_prev_results_state['checked_heuristic'],
            $backup_prev_results_state['checked_signatures'],
            $backup_prev_results_state['status'],
            $backup_prev_results_state['severity'],
            $backup_prev_results_state['path']
        )
    ) {
        $backup_prev_results_state = null;
    }

    if (is_null($backup_path)) {
        return array('error' => esc_html__('Error: Backup not found.', 'security-malware-firewall'));
    }

    $full_backup_path = ABSPATH . ltrim($backup_path, '/');

    // Trying to replace backup and original file
    $backup_content = file_get_contents($full_backup_path);

    if ($backup_content === false) {
        return array('error' => esc_html__('Error: File not exists or permissions denied.', 'security-malware-firewall'));
    }

    if (!file_exists($full_file_path)) {
        return array('error' => esc_html__('Error: Original file not exists.', 'security-malware-firewall'));
    }

    $result = file_put_contents($full_file_path, $backup_content);

    if ($result === false) {
        return array('error' => esc_html__('Error: Permissions denied.', 'security-malware-firewall'));
    }

    // Success: remove all data about backup
    $error_msg = array(
        spbct_delete_backup_file($full_backup_path),
        spbct_remove_backup_from_db($file_path),
        spbct_update_cure_log($file_path),
        spbct_update_scan_results($file_path, $backup_prev_results_state)
    );

    foreach ($error_msg as $value) {
        if (array_key_exists('error', $value)) {
            return $value;
        }
    }

    return array('success' => true);
}

/**
 * Deleting a backup file
 * @param string $full_backup_path
 * @return array
 */
function spbct_delete_backup_file($full_backup_path)
{
    if ($full_backup_path) {
        if (!unlink($full_backup_path)) {
            return array('error' => esc_html__('Deleting backup error: Permissions denied.', 'security-malware-firewall'));
        }
    } else {
        return array('error' => esc_html__('Deleting backup error: Incorrect backup file path.', 'security-malware-firewall'));
    }
    return array();
}

/**
 * Deleting a backup from a database
 * @param string $file_path
 * @return array
 */
function spbct_remove_backup_from_db($file_path)
{
    global $wpdb;
    if ($file_path) {
        $sql_prepared = $wpdb->prepare(
            'DELETE FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE real_path = %s;',
            $file_path
        );
        if ($wpdb->query($sql_prepared) === false) {
            return array('error' => esc_html__('Deleting backup error: Something is wrong during deleting backup.', 'security-malware-firewall'));
        }
    } else {
        return array('error' => esc_html__('Deleting backup error: Incorrect backup file path for the database query.', 'security-malware-firewall'));
    }
    return array();
}

/**
 * Changing the file information in the cure_log table regarding file recovery
 * @param string $file_path
 * @return array
 */
function spbct_update_cure_log($file_path)
{
    global $wpdb;
    if ($file_path) {
        $sql_prepared = $wpdb->prepare(
            'UPDATE ' . SPBC_TBL_CURE_LOG . ' SET is_restored = 1, cure_status = 0 WHERE real_path = %s;',
            $file_path
        );
        if ($wpdb->query($sql_prepared) === false) {
            return array('error' => esc_html__('Error update cure log: Something is wrong during updating cure log.', 'security-malware-firewall'));
        }
    } else {
        return array('error' => esc_html__('Error update cure log: Incorrect backup file path for the database query.', 'security-malware-firewall'));
    }
    return array();
}

/**
 * Changing file information after recovery
 * @param string $file_path
 * @param array $backup_prev_results_state
 * @return array
 */
function spbct_update_scan_results($file_path, $backup_prev_results_state)
{
    global $wpdb;
    if ($file_path && $backup_prev_results_state !== null) {
        $sql_prepared = $wpdb->prepare(
            'UPDATE ' . SPBC_TBL_SCAN_FILES . ' SET weak_spots = %s, checked_heuristic = %s, checked_signatures = %s, status = %s, severity = %s WHERE path = %s;',
            array(
                $backup_prev_results_state['weak_spots'],
                $backup_prev_results_state['checked_heuristic'],
                $backup_prev_results_state['checked_signatures'],
                $backup_prev_results_state['status'],
                $backup_prev_results_state['severity'],
                $file_path
            )
        );
        if ($wpdb->query($sql_prepared) === false) {
            return array('error' => esc_html__('Error update scan results: Something is wrong during saving previous state of file.', 'security-malware-firewall'));
        }
    } else {
        return array('error' => esc_html__('Error update scan results: Incorrect data for changing the recovery results about the file', 'security-malware-firewall'));
    }
    return array();
}

/**
 * Restore file from backup AJAX action handler
 * @return void|string|WP_Error
 */
function spbc_restore_file_from_backup_ajax_action()
{
    spbc_check_ajax_referer('spbc_secret_nonce', 'security');

    $file_fast_hash = isset($_POST['file_fast_hash']) ? esc_sql($_POST['file_fast_hash']) : null;

    $restore_result = spbc_restore_file_from_backup_ajax_action_handler($file_fast_hash);

    if (isset($restore_result['success'])) {
        wp_send_json_success(esc_html__('Success!', 'security-malware-firewall'));
    } elseif (isset($restore_result['error'])) {
        wp_send_json_error($restore_result['error']);
    }
}

/**
 * @param $paths array
 *
 * @return array
 */
function spbc__get_exists_directories($paths)
{
    $exists_dirs = array();

    foreach ($paths as $path) {
        if (is_dir(ABSPATH . $path)) {
            $exists_dirs[] = $path;
        }
    }

    return $exists_dirs;
}

/**
 * Get JSON string of accordion row actions that do not need to be confirmed.
 * @return string
 */
function spbc_get_no_confirm_row_actions()
{
    global $spbc;
    // by defaults
    $actions = array (
        'defaults' => array(
            'copy_file_info',
            'check_analysis_status',
        ),
        'restricted' => array(),
    );
    // if license is trial
    if ($spbc->data['license_trial']) {
        $actions['restricted'][] = 'delete';
        $actions['restricted'][] = 'quarantine';
        $actions['restricted'][] = 'approve';
    }

    $actions['any'] = array_merge($actions['restricted'], $actions['defaults']);

    $actions = json_encode($actions);
    return is_string($actions) ? $actions : '{[]}';
}