<?php

namespace CleantalkSP\SpbctWP\Scanner;

use CleantalkSP\SpbctWP\Scanner\CureLog\CureLog;
use CleantalkSP\SpbctWP\Scanner\OSCron\OSCronModel;
use CleantalkSP\SpbctWP\Scanner\ScanningLog\ScanningLogFacade;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\ScannerFileStatuses;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\ScanningStagesStorage;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\FileSystemAnalysis;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\FrontendAnalysis;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\GetApprovedHashes;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\GetCmsHashes;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\GetDeniedHashes;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\GetModulesHashes;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\HeuristicAnalysis;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\OSCronAnalysis;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\OutboundLinks;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\ScheduleSendHeuristicSuspiciousFiles;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\SignatureAnalysis;
use CleantalkSP\SpbctWP\Scanner\Stages\SignatureAnalysis\SignatureAnalysisFacade;
use CleantalkSP\SpbctWP\Scanner\UnsafePermissionsModule\UnsafePermissionsHandler;
use CleantalkSP\SpbctWP\Scanner\ScannerInteractivity\ScannerInteractivityData;
use CleantalkSP\Variables\Request;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\Cron;
use CleantalkSP\SpbctWP\Transaction;
use CleantalkSP\SpbctWP\State;
use CleantalkSP\SpbctWP\API;
use CleantalkSP\SpbctWP\Helpers\HTTP;
use CleantalkSP\SpbctWP\Helpers\Helper as QueueHelper;
use CleantalkSP\SpbctWP\Helpers\CSV;
use CleantalkSP\SpbctWP\RemoteCalls;
use CleantalkSP\SpbctWP\Scanner\Stages\SendResultsStage;
use CleantalkSP\SpbctWP\Scanner\Stages\CureStage;

class ScannerQueue
{
    /**
     * @var string[] List of scan stages
     */
    public static $stages = array(
        'get_cms_hashes',
        'get_modules_hashes',
        'clean_results',
        'file_system_analysis',
        'os_cron_analysis',
        'get_denied_hashes',
        'get_approved_hashes',
        'signature_analysis',
        'heuristic_analysis',
        'schedule_send_heuristic_suspicious_files',
        'auto_cure_backup',
        'auto_cure',
        'outbound_links',
        'frontend_analysis',
        'important_files_listing',
        'send_results',
    );

    /**
     * @var string Site root directory
     */
    private $root;

    /**
     * @var int Current action offset
     */
    private $offset;

    /**
     * @var int Amount of elements to process in current action
     */
    private $amount;

    /**
     * @var string Current scan stage
     */
    private $stage;

    /**
     * @var DB
     */
    private $db;

    /**
     * Max queue iteration size for signature analysis.
     * @var int
     */
    private static $max_queue_files_aggregated_size__signatures = 2097152;
    /**
     * Max queue iteration size for heuristic analysis.
     * @var int
     */
    private static $max_queue_files_aggregated_size__heuristic = 524288;

    public function __construct($stage = '', $offset = null, $amount = null, $root_dir = null)
    {
        global $spbc;

        @set_time_limit(120); // Increasing Script execution time

        $this->db = DB::getInstance();

        $this->stage  = isset($stage) ? $stage : Request::get('stage');
        $this->amount = isset($amount) ? (int)$amount : (int)Request::get('amount');
        $this->offset = isset($offset) ? (int)$offset : (int)Request::get('offset');
        $this->root   = $root_dir ?: realpath(ABSPATH);

        // Crunch for cure backups
        if ( isset($spbc->settings['scanner__auto_cure']) && $spbc->settings['scanner__auto_cure'] == 0) {
            unset(self::$stages['auto_cure_backup']);
        }

        // Skip send_heuristic_suspicious_files if setting is disabled
        if ( isset($spbc->settings['scanner__schedule_send_heuristic_suspicious_files']) && $spbc->settings['scanner__schedule_send_heuristic_suspicious_files'] == 0) {
            unset(self::$stages['schedule_send_heuristic_suspicious_files']);
        }
    }

    /**
     * Launches background scanning by making a remote call
     * Also set a cron task for each 30 seconds in case RC failing
     * Set a transaction 'background_scan' and pass it to RC and Cron task
     *
     * @return bool|string[]
     */
    public static function launchBackground()
    {
        global $spbc;

        $transaction_id = Transaction::get('background_scanner', 3600 * 2)->perform();

        if (
            ! $spbc->moderate ||
            ! $spbc->settings['scanner__auto_start'] ||
            ! $transaction_id
        ) {
            return true;
        }

        $params = array(
            'transaction_id' => $transaction_id,
            'stage'          => 'get_cms_hashes',
            'offset'         => 0,
        );

        //remove old task and set a new
        Cron::updateTask(
            'background_scan',
            'spbc_scanner__controller',
            30,
            time() + 30,
            $params
        );

        // Remove link for shuffle salts
        $spbc->settings['there_was_signature_treatment'] = 0;
        $spbc->save('settings');

        // Do not return the value because it could alter a scheduled scan time
        // If fails scan will run the next scheduled time
        return RemoteCalls::performToHost(
            'scanner__controller',
            $params,
            array('async', 'get')
        );
    }

    /**
     * Description here
     *
     * @param int|null $transaction_id Transaction ID passed directly
     * @param string|null $stage
     * @param int $offset
     * @param int $amount
     *
     * @return bool|string|string[]
     * @global State $spbc
     */
    public static function controllerBackground($transaction_id = null, $stage = null, $offset = null, $amount = null)
    {
        global $spbc;

        $transaction_id = $transaction_id ?: Request::get('transaction_id'); // @todo cast to int by Variables

        if ( (int)$transaction_id !== (int)Transaction::get('background_scanner')->getTID() ) {
            return true;
        }

        $self = new self($stage, $offset, $amount);
        if ( ! method_exists($self, $self->stage) ) {
            return array('error' => 'controllerBackground: UNKNOWN_METHOD: ' . $self->stage);
        }

        $result = $self->{$self->stage}();

        // Handling errors
        $spbc->error_toggle(! empty($result['error']), 'cron_scan', $result);
        if ( ! empty($result['error']) ) {
            return $result;
        }

        // Current stage is ended. Preparing for next.
        if ( $result['end'] ) {
            $self->stage = $self->getNextStage($self->stage);
        }

        // New stage is unset. End of scanning.
        if ( $self->stage === null ) {
            // End the transaction cause the scanning is finished
            Transaction::get('background_scanner')->clearTransactionTimer();
            Cron::removeTask('background_scan');

            return true;
        }

        switch ( $self->stage ) {
            case 'get_modules_hashes':
                $self->amount = 20;
                break;
            case 'frontend_analysis':
                $self->amount = (defined('SPBCT_ALLOW_CURL_SINGLE') && SPBCT_ALLOW_CURL_SINGLE) ? 2 : 20;
                break;
            case 'clean_results':
                $self->amount = 10000;
                break;
            case 'file_system_analysis':
                $self->amount = 700;
                break;
            case 'heuristic_analysis':
                $self->amount = 4;
                break;
            case 'auto_cure':
                $self->amount = 5;
                break;
            case 'signature_analysis':
            case 'outbound_links':
                $self->amount = 10;
                break;
        }

        $params = array(
            'transaction_id' => $transaction_id,
            'stage'          => $self->stage,
            'offset'         => $result['end'] ? 0 : $self->offset + $result['processed'],
            'amount'         => $self->amount,
        );

        Cron::updateTask(
            'background_scan',
            'spbc_scanner__controller',
            30,
            time() + 30,
            $params
        );

        // Need to halt the script because of cooldown for remote call 'scanner__controller' (2 seconds)
        sleep(2);


        $rc_result = RemoteCalls::performToHost(
            'scanner__controller',
            $params,
            array('async', 'get')
        );

        return $rc_result;
    }

    public function getNextStage($stage)
    {
        global $spbc;

        // Check if the passed and next state is set
        if ( ! isset(self::$stages[$stage]) && ! isset(self::$stages[(int)array_search($stage, self::$stages, true) + 1]) ) {
            return null;
        }

        $stage   = self::$stages[(int)array_search($stage, self::$stages, true) + 1];
        $setting = 'scanner__' . $stage;

        /**
         * Recursion.
         * Check if the next stage is disabled by setting
         * If so, get the next one
         */
        if ( isset($spbc->settings[$setting]) && (int)$spbc->settings[$setting] === 0 ) {
            $stage = $this->getNextStage($stage);
        }

        return $stage;
    }

    public static function controllerFront()
    {
        if ( ! spbc_check_ajax_referer('spbc_secret_nonce', 'security', false) ) {
            wp_send_json(array('error' => 'Nonce had been changed. Please, restart the scan.'));
        }

        $scanner     = new self();
        $method_name = str_replace('spbc_scanner_', '', Request::get('method'));

        $out = method_exists(__CLASS__, $method_name)
            ? $scanner->$method_name()
            : array('error' => 'UNKNOWN_ACTION');
        wp_send_json($out);
    }

    /**
     * Receive CMS hash
     *
     * @global string $wp_version
     * @global State $spbc
     *
     * @return array
     */
    public function get_cms_hashes() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc, $wp_version;

        $spbc->data['scanner']['scan_start_timestamp'] = time();
        //use wordpress native fucntion to get localized time for scanner start
        $spbc->data['scanner']['scanner_start_local_date'] = current_time('Y-m-d H:i:s');
        $spbc->save('data');

        // Clearing old data about scanning stages
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->reset();
        ScanningLogFacade::clearLog();

        if ( preg_match('/^\d*\.?\d*\.?\d*$/', $wp_version) === 1 ) {
            $stage_data_obj = $scanning_stages_storage->getStage(GetCmsHashes::class);

            if (
                ! isset($spbc->data['scanner']['last_wp_version'])
                || (isset($spbc->data['scanner']['last_wp_version']) && $spbc->data['scanner']['last_wp_version'] !== $wp_version)
                || ! $this->db->execute('SELECT path FROM ' . SPBC_TBL_SCAN_FILES . ' LIMIT 1')
            ) {
                // Getting hashes
                $result = \CleantalkSP\SpbctWP\Scanner\Helper::getHashesForCMS('wordpress', $wp_version);

                if ( empty($result['error']) ) {
                    $this->db->execute('DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE source_type = "CORE";');
                    $is_windows        = $spbc->is_windows ? true : false;
                    $data              = array();
                    $missed_cms_hashes = array();
                    $expected_count_hashes = $result['checksums_count'];

                    foreach ( $result['checksums'] as $path => $real_full_hash ) {
                        $path      = $is_windows ? str_replace('/', '\\', $path) : $path;
                        $fast_hash = md5($path);
                        $path      = addslashes($path);
                        $data[]    = sprintf(
                            '("%s","%s","%s","CORE", "wordpress", "%s", "1", "1", "OK")',
                            $fast_hash,
                            $path,
                            $real_full_hash,
                            $wp_version
                        );
                        //collect if there are still some files (fix for cron launch after WP updated)
                        $missed_cms_hash = $this->db->fetchAll(
                            'SELECT fast_hash FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash = "' . $fast_hash . '";'
                        );
                        if ( $missed_cms_hash ) {
                            $missed_cms_hashes[] = '\'' . $missed_cms_hash[0]['fast_hash'] . '\'';
                        }
                    }
                    //if missed hashes found delete them
                    if ( ! empty($missed_cms_hashes) ) {
                        $this->db->execute(
                            'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash in (' . implode(
                                ',',
                                $missed_cms_hashes
                            ) . ');'
                        );
                    }

                    $sql = 'INSERT INTO ' . SPBC_TBL_SCAN_FILES . ' (`fast_hash`, `path`, `real_full_hash`, `source_type`, `source`, `version`, `checked_heuristic`, `checked_signatures`, `status`) VALUES ';

                    $result = $this->db->execute($sql . implode(',', $data) . ';');

                    if ( $result !== false ) {
                        // save data to scanning stages log
                        $stage_data_obj->set('expected_count_hashes', $expected_count_hashes);
                        $stage_data_obj->set('added_count_hashes', $result);

                        $out = array(
                            'end'         => 1,
                            'processed'   => $result,
                            'files_count' => $result
                        );
                    } else {
                        $out['error'] = 'COULDNT_INSERT with error: ' . $this->db->getLastError();
                    }

                    $spbc->data['scanner']['last_wp_version'] = $wp_version;
                    $spbc->error_delete('get_hashes', true);
                    $spbc->save('data');
                } else {
                    $out = $result;
                }
            } else {
                // not the first scan from a client
                $sql = 'SELECT COUNT(*) AS cnt FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE source_type = "CORE";';
                $result = $this->db->fetch($sql, OBJECT);
                $stage_data_obj->set('expected_count_hashes', 0);
                $stage_data_obj->set('added_count_hashes', $result->cnt);
                $out = array('comment' => 'Already up to date.', 'end' => 1,);
            }

            $scanning_stages_storage->saveToDb();

            // Adding to log
            ScanningLogFacade::writeToLog(
                '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
            );

            // Sending data to frontend
            $out['stage_data_for_logging'] = array(
                'title' => $stage_data_obj::getTitle(),
                'description' => $stage_data_obj->getDescription()
            );
        } else {
            $out = array('error' => 'Your WordPress version is not supported');
        }

        return $out;
    }

    /**
     * Count total amount of plugins and themes
     *
     * @return array
     * @global State $spbc
     *
     */
    public function countModules()
    {
        global $spbc;

        $out = array(
            'total'   => 0,
            'plugins' => 0,
            'themes'  => 0,
        );

        foreach ( array('plugins', 'themes') as $modules_type ) {
            // Preparing modules to check again
            $system_modules = spbc_get_modules_by_type($modules_type);
            $spbc->$modules_type;
            if ( empty($spbc->$modules_type) ) {
                $spbc->$modules_type = $system_modules;
            }

            foreach ( array_keys($system_modules) as $system_module_slug ) {
                if ( isset($spbc->{$modules_type}[$system_module_slug]) ) {
                    //reassign from state to make code readable
                    $state_module_to_modify = $spbc->{$modules_type}[$system_module_slug];
                    if ( ! is_array($state_module_to_modify) ) {
                        continue;
                    }
                    // has no checked property - simple counting
                    if ( empty($state_module_to_modify['checked']) ) {
                        $out['total']++;
                        $out[$modules_type]++;
                    }
                    // has should_be_checked_again property - unset prop, set as not checked and counting
                    if ( ! empty($state_module_to_modify['should_be_checked_again']) ) {
                        $state_module_to_modify['checked'] = 0;
                        $out['total']++;
                        $out[$modules_type]++;
                        unset($state_module_to_modify['should_be_checked_again']);
                    }
                    // if versions is differ - make it unchecked and count
                    if (
                        isset(
                            $state_module_to_modify['Version'],
                            $system_modules[$system_module_slug]['Version']
                        ) &&
                        $state_module_to_modify['Version'] !== $system_modules[$system_module_slug]['Version']
                    ) {
                        $state_module_to_modify['checked'] = 0;
                        $out['total']++;
                        $out[$modules_type]++;
                    }
                    // reassign simplified var to state
                    $spbc->{$modules_type}[$system_module_slug] = $state_module_to_modify;
                } else {
                    $out['total']++;
                    $out[$modules_type]++;
                }
            }
            // save all the modules to state
            $spbc->save($modules_type, true, false);
        }

        return $out;
    }

    /**
     * @param int $amount
     * @param int $offset
     *
     * @return array
     * @global State $spbc
     *
     * @global State $spbc
     */
    public function get_modules_hashes($amount = null, $offset = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $amount = isset($amount) ? (int)$amount : $this->amount;
        $offset = isset($offset) ? (int)$offset : $this->offset;

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

        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(GetModulesHashes::class);

        // Count modules and add it to output
        if ( $offset === 0 ) {
            $count_modules = $this->countModules();

            $stage_data_obj->set('count_plugins', count(spbc_get_modules_by_type('plugins')));
            $stage_data_obj->set('count_themes', $count_modules['themes']);

            $out = array_merge($out, $count_modules);
        }

        //return false on the hook to prevent errors from other actions bound to the hook
        add_filter('plugins_api', function ($_res) {
            return false;
        }, 9999, 1);

        foreach ( array('plugins', 'themes') as $modules_type ) {
            // Attaching API functions
            if ( $modules_type === 'plugins' ) {
                require_once(ABSPATH . 'wp-admin/includes/plugin-install.php');
            }

            if ( $modules_type === 'themes' ) {
                require_once(ABSPATH . 'wp-admin/includes/theme.php');
            }

            // Get all modules
            $modules               = spbc_get_modules_by_type($modules_type);
            $modules_type_singular = substr($modules_type, 0, -1);
            $modules_dir           = spbc_get_module_folder_by_type($modules_type);
            $is_windows            = $spbc->is_windows;

            // @todo crunch. this calls magic method __get on $spbc->plugins or $spbc->themes property.
            $spbc->$modules_type;

            // Fix for the first start
            if ( is_array($spbc->$modules_type) ) {
                $spbc->$modules_type = new \ArrayObject($spbc->$modules_type);
            }

            foreach ( $modules as $module_slug => $module ) {
                if ( $out['processed'] >= $amount ) {
                    break;
                }

                if (
                    ! empty($spbc->{$modules_type}[$module_slug]['checked']) &&
                    ! empty($spbc->{$modules_type}[$module_slug]['Version']) &&
                    $spbc->{$modules_type}[$module_slug]['Version'] === $module['Version']
                ) {
                    continue;
                }

                $spbc->{$modules_type}[$module_slug] = $module;

                $modules_entry_type = $modules_type === 'plugins' ? 'plugin_information' : 'theme_information';

                if ( $modules_entry_type === 'plugin_information' ) {
                    if (version_compare(phpversion(), '7.0.0', '>')) {
                        try {
                            $result_wp_api_modules = plugins_api(
                                $modules_entry_type,
                                array('slug' => $module_slug, 'fields' => array('Version' => true))
                            );
                        } catch (\Exception $e) {
                            error_log($e->getMessage(), 1);
                        }
                    } else {
                        $result_wp_api_modules = plugins_api(
                            $modules_entry_type,
                            array('slug' => $module_slug, 'fields' => array('Version' => true))
                        );
                    }
                } else {
                    $result_wp_api_modules = themes_api(
                        $modules_entry_type,
                        array('slug' => $module_slug, 'fields' => array('Version' => true))
                    );
                }

                if ( ! is_wp_error($result_wp_api_modules) && isset($result_wp_api_modules->version) ) {
                    // Not error, version exists
                    $source_status = (version_compare(
                        $module['Version'],
                        $result_wp_api_modules->version,
                        '>='
                    ) ? 'UP_TO_DATE' : 'OUTDATED');
                } elseif ( ! is_wp_error($result_wp_api_modules) && ! isset($result_wp_api_modules->version) ) {
                    // Not error, version NOT exists
                    $source_status = 'UNKNOWN';
                } else {
                    // Error
                    $source_status = ($result_wp_api_modules->get_error_message(
                    ) === 'Plugin not found.' ? 'NOT_IN_DIRECTORY' : 'UNKNOWN');
                }

                $out['outdated']                   = $source_status === 'OUTDATED';
                $out['checked_' . $modules_type][] = $module_slug;

                // Get Cleantalk's hash
                $result_hashes = \CleantalkSP\SpbctWP\Scanner\Helper::getHashesForModules(
                    'wordpress',
                    $modules_type_singular,
                    $module_slug,
                    $module['Version']
                );

                // Remove approved files
                $approved_real_full_hashes = ScanRepository::getApprovedRealFullHashes();

                if ($approved_real_full_hashes) {
                    foreach ($result_hashes as $key => $data) {
                        if (in_array($data[1], $approved_real_full_hashes)) {
                            unset($result_hashes[$key]);
                        }
                    }
                }

                if ( empty($result_hashes['error']) ) {
                    $this->db->execute(
                        'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE path LIKE "%' . $module_slug . '%" AND status <> "APPROVED_BY_USER";'
                    );
                    $sql_hat    = 'INSERT INTO ' . SPBC_TBL_SCAN_FILES . ' (`fast_hash`, `path`, `real_full_hash`, `source_type`, `source`, `source_status`, `version`, `checked_heuristic`, `checked_signatures`, `status`) VALUES ';
                    $sql_values = array();
                    foreach ( $result_hashes as $value ) {
                        $path           = '/' . substr($modules_dir . '/' . $value[0], strlen(ABSPATH));
                        $path           = $is_windows ? str_replace('/', '\\', $path) : $path;
                        $fast_hash      = md5($path);
                        $path           = addslashes($path);
                        $real_full_hash = $value[1];
                        $sql_values[]   = "('$fast_hash', '$path', '$real_full_hash', '"
                                          . strtoupper($modules_type_singular)
                                          . "', '$module_slug', '$source_status', '{$module['Version']}', '1', '1', 'OK')";
                    }
                    if ( $sql_values ) {
                        $this->db->execute($sql_hat . implode(',', $sql_values));
                    }
                } else {
                    if ($modules_type === 'plugins') {
                        $stage_data_obj->increase('count_plugins_without_hashes', 1);
                    }
                    if ($modules_type === 'themes') {
                        $stage_data_obj->increase('count_themes_without_hashes', 1);
                    }

                    // Cloud should refresh the hash for this module
                    if ( $result_hashes['error'] === 'REMOTE_FILE_NOT_FOUND_OR_VERSION_IS_NOT_SUPPORTED__PLUG' ) {
                        $to_refresh['wordpress'][$modules_type][] = array(
                            'name'    => $module_slug,
                            'version' => $module['Version'],
                        );
                    }

                    // Saving it.
                    $spbc->{$modules_type}[$module_slug]['error'] = $result_hashes['error'];
                }

                if (
                    $source_status === 'NOT_IN_DIRECTORY' ||
                    $source_status === 'UNKNOWN' ||
                    ! empty($spbc->{$modules_type}[$module_slug]['error'])
                ) {
                    $spbc->{$modules_type}[$module_slug]['should_be_checked_again'] = true;
                }

                $out['processed']++;
                $spbc->{$modules_type}[$module_slug]['checked'] = true;
            }

            $spbc->save($modules_type, true, false);
        }

        if ( ! empty($to_refresh) ) {
            $to_refresh = json_encode($to_refresh);
            API::method__request_checksums($spbc->settings['spbc_key'], $to_refresh);
        }

        $out['end'] = $out['processed'] < $amount ? 1 : 0;

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        return $out;
    }

    /**
     * Delete non-existing files from table (except quarantined files)
     *
     * @param int $offset
     * @param int $amount
     *
     * @return mixed
     */
    public function clean_results($offset = null, $amount = 50000) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        $offset = isset($offset) ? $offset : $this->offset;
        $amount = isset($amount) ? $amount : $this->amount;

        global $spbc;

        $result = $this->db->fetchAll(
            'SELECT path, fast_hash, status'
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . " LIMIT $offset, $amount;"
        );

        $to_delete = array();
        foreach ( $result as $value ) {
            if ( $value['status'] !== 'QUARANTINED' && ! file_exists($this->root . $value['path']) ) {
                $to_delete[] = $this->db->prepare('%s', $value['fast_hash'])->getQuery();
            }
        }

        $deleted = 0;
        if ( ! empty($to_delete) ) {
            $deleted = $this->db->execute(
                'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE fast_hash IN (' . implode(',', $to_delete) . ');'
            );
        }

        // Deleting newly added exclusions
        $this->deleteFilesOfExclusionDirs($spbc->settings['scanner__dir_exclusions']);

        $out = array(
            'total'     => (int)$deleted,
            'processed' => (int)$deleted,
            'deleted'   => (int)$deleted,
            'end'       => 1,
        );

        if ( $deleted === false ) {
            $out['error'] = 'COULDNT_DELETE';
        }

        return $out;
    }

    /**
     * Count files in the tables by given 'status' and 'checked' columns
     *
     * @param string $status
     * @param string $caller
     *
     * @return array
     */
    public function countFilesByStatusAndChecked($status = '', $caller = '')
    {
        $status = stripslashes(Request::get('status')) ?: $status;
        //$checked = stripslashes( Request::get('checked') ) ?: $checked; #todo What was this for?
        if ( Request::get('checked') ) {
            error_log(
                'countFilesByStatusAndChecked: $_GET[\'checked\'] parameter found, but not handled in the method ' . var_export(
                    $_GET,
                    true
                )
            );
        }

        if ( ! preg_match('#^[A-Z,_]+$#', $status) ) {
            return array('error' => 'BAD_PARAMS', 'comment' => "status: $status");
        }

        if ( ! preg_match('#^[A-Z,_]+$#', $caller) ) {
            return array('error' => 'BAD_PARAMS', 'comment' => "caller: $caller");
        }

        $status = is_string($status) ? explode(',', $status) : $status;
        $status = '"' . implode('","', $status) . '"';

        $caller = $caller === 'SIGNATURE_ANALYSIS' ? 'checked_signatures' : 'checked_heuristic';
        $query  =
            'SELECT COUNT(fast_hash) AS cnt'
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . ' WHERE ' . $caller . " = '0' AND status IN (" . $status . ');';// No need to validate or sanitize, already did
        $result = $this->db->fetch($query);

        return $result !== null
            ? array('total' => (int)$result->cnt)
            : array(
                'error'   => __FUNCTION__ . ' query error',
                'comment' => substr($this->db->getLastError(), 0, 1000),
            );
    }

    /**
     * Scan file system for alterations
     * Save it to DB/Storage
     *
     * @param int $offset
     * @param int $amount
     * @param string $path_to_scan
     *
     * @return array|string[]
     */
    public function file_system_analysis($offset = null, $amount = null, $path_to_scan = ABSPATH) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $offset       = isset($offset) ? $offset : $this->offset;
        $amount       = isset($amount) ? $amount : $this->amount;
        $path_to_scan = realpath($path_to_scan);
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(FileSystemAnalysis::class);

        $init_params = array(
            'fast_hash'             => true,
            'full_hash'             => true,
            'offset'                => $offset,
            'amount'                => $amount,
            'extensions'            => 'php, html, htm, php2, php3, php4, php5, php6, php7, phtml, shtml, phar, odf, [ot.]',
            'extensions_exceptions' => '', //array('jpg', 'jpeg', 'png', 'gif', 'css', 'txt', 'zip', 'xml', 'json')
            'file_exceptions'       => 'wp-config.php',
            'files_mandatory'       => array(),
            'dir_exceptions'        => array(SPBC_PLUGIN_DIR . 'quarantine'),
            // important!
            'running_due_stage'     => true
        );

        if ( ! empty($spbc->settings['scanner__dir_exclusions']) ) {
            $init_params['dir_exceptions'] = array_merge(
                $init_params['dir_exceptions'],
                spbc__get_exists_directories(explode("\n", $spbc->settings['scanner__dir_exclusions']))
            );
        }

        $scanner = new Surface($path_to_scan, $this->root, $init_params);

        if ($scanner->has_errors) {
            return array('error' => __FUNCTION__ . ' Surface analysis internal error.');
        }

        if ($offset === 0 && $scanner->stage_end && ! $scanner->output_files_count) {
            return array('error' => __FUNCTION__ . ' No files to scan');
        }

        if ( $scanner->output_files_count ) {
            $stage_data_obj->increase('scanned_count_files', $scanner->output_files_count);
            $sql_query__values           = array();
            $sql_query__values_non_ascii = array();
            //should be offset
            $detected_at                 = current_time('timestamp');
            $sql_hat                     = 'INSERT INTO ' . SPBC_TBL_SCAN_FILES
                                           . ' (`path`, `size`, `perms`, `mtime`, `fast_hash`, `full_hash`, `detected_at`, `checked_heuristic`) VALUES ';

            foreach ( $scanner->output_files as $_key => $file ) {
                // skip restored files as is
                $cure_log = new CureLog();
                $restored = $cure_log->getRestoredFiles();
                if (!empty($restored)) {
                    foreach ($restored as $restored_file) {
                        if ($restored_file['real_path'] === $file['path']) {
                            continue 2;
                        }
                    }
                }
                $file['path']        = trim($this->db->prepare('%s', $file['path'])->getQuery(), '\'');
                $file['detected_at'] = $detected_at;

                //JS files exclusion
                $ext = @pathinfo($file['path'], PATHINFO_EXTENSION);
                $file['checked_heuristic'] = $ext === 'js' ? 1 : 0;
                //JS files end

                if ( ! spbc_check_ascii($file['path']) ) {
                    $sql_query__values_non_ascii[] = '(\'' . implode('\',\'', $file) . '\')';
                } else {
                    $sql_query__values[] = '(\'' . implode('\',\'', $file) . '\')';
                }
            }

            $sql_suffix = " ON DUPLICATE KEY UPDATE
			
			size        = VALUES(`size`),
			perms       = VALUES(`perms`),
			source      = source,
			source_type = source_type,
			version     = version,

			fast_hash = fast_hash,
			full_hash = VALUES(`full_hash`),
			real_full_hash = real_full_hash,
			
			checked_signatures =
				IF(real_full_hash IS NOT NULL AND real_full_hash = VALUES(`full_hash`),
					1,
					IF(mtime <> VALUES(`mtime`) OR mtime IS NULL,
						0,
						checked_signatures
					)
				),
				
			checked_heuristic =
				IF(real_full_hash IS NOT NULL AND real_full_hash = VALUES(`full_hash`),
					1,
					IF(mtime <> VALUES(`mtime`) OR mtime IS NULL,
						0,
						checked_heuristic
					)
				),
			
			status =
				IF(mtime <> VALUES(`mtime`) OR mtime IS NULL,
					IF(real_full_hash IS NULL,
						IF(checked_heuristic = 1 OR checked_signatures = 1,
							status,
							'UNKNOWN'
						),
						IF(real_full_hash = VALUES(`full_hash`),
							'OK',
							'MODIFIED'
						)
					),
					status
				),
			
			mtime     = VALUES(`mtime`),
			
			detected_at = IF(
			    detected_at IS NULL,
			    VALUES(`detected_at`),
			    detected_at
            ),
			
			severity  =
				IF(
				    (status <> 'OK' AND (checked_heuristic = 1 OR checked_signatures = 1)),
					severity,
					NULL
				),
				
			weak_spots  =
				IF(
				    (status <> 'OK' AND (checked_heuristic = 1 OR checked_signatures = 1)),
					weak_spots,
					NULL
				);";

            if ( $sql_query__values ) {
                $success = $this->db->execute($sql_hat . implode(',', $sql_query__values) . $sql_suffix);
            }
            if ( $sql_query__values_non_ascii ) {
                // @todo Resolve conflict with non ASCII symbol path names. Right now DB errors is suppressed for this cases.
                @$this->db->execute($sql_hat . implode(',', $sql_query__values_non_ascii) . $sql_suffix);
            }
        }

        if ( isset($success) ) {
            if ( $success === false ) {
                $output = array(
                    'error'   => __FUNCTION__ . ' DataBase write error while scanning files.',
                    'comment' => substr($this->db->getLastError(), 0, 1000),
                );
                if ( $spbc->debug ) {
                    spbc_log($this->db->getLastQuery());
                }
            } else {
                $output = array(
                    'processed'   => $scanner->output_files_count,
                    'files_count' => $scanner->output_files_count,
                    'dirs_count'  => $scanner->dirs_count,
                    'offset'      => $offset,
                    'amount'      => $amount,
                    'end'         => false,
                );

                // End of stage
                if ($scanner->stage_end) {
                    $output['end'] = true;
                    // Checking unsafe permissions
                    $unsafe_permissions_handler = new UnsafePermissionsHandler();
                    $unsafe_permissions_handler->handle();
                }
            }
        }

        // Count files if it's first iteration
        if ( $offset === 0 ) {
            $init_params['count'] = true;
            unset($init_params['amount'], $init_params['offset']);
            $scanner         = new Surface($path_to_scan, $this->root, $init_params);
            if ($scanner->has_errors) {
                return array('error' => __FUNCTION__ . ' Counting files internal error.');
            }
            $output['total'] = $scanner->output_files_count;
        }

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $output['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        return $output;
    }

    /**
     * Check crontab for scheduled scans
     *
     * @return array
     * @psalm-suppress ForbiddenCode
     */
    public function os_cron_analysis() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(OSCronAnalysis::class);

        $result = OSCronModel::run();

        if (true !== $result) {
            ScanningLogFacade::writeToLog(
                '<b>' . $stage_data_obj::getTitle() . '</b> ' . $result
            );

            return array(
                'end'   => 1,
            );
        }

        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        return array('end' => 1);
    }

    /**
     * Getting remote hashes of denied files
     *
     * @return array
     */
    public function get_denied_hashes() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $warnings_before_iteration = spbc_scanner_get_files_by_category('critical');

        $result = \CleantalkSP\SpbctWP\Scanner\Helper::getHashesForFiles('wordpress', 'denied', '1.0.0');

        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(GetDeniedHashes::class);

        if ( empty($result['error']) ) {
            $spbc->data['scanner']['checksums_count_ct'] = count($result);
            $spbc->save('data');

            $where = implode('\',\'', $result);
            if ( ! preg_match('#^[a-zA-Z0-9\',]+$#', $where) ) {
                return array('error' => 'BAD_PARAMS');
            }

            /**
             * Set unknown status if the file was denied before, but its has does not
             * persist in the denied hashes list for now. This implemented for cases where we
             * do change hashes list on mistakenly accepted wrong verdict
             */
            $this->db->execute(
                'UPDATE ' . SPBC_TBL_SCAN_FILES
                . ' SET
                checked_heuristic = 0,
                checked_signatures = 0,
                severity =  NULL,
                status = \'UNKNOWN\',
                weak_spots = \'\'
                WHERE status   =   \'DENIED_BY_CT\'
                AND full_hash NOT IN (\'' . $where . '\')
                AND status <> \'QUARANTINED\';'
            );

            /**
             * Update the db depend on current hashes list.
             */
            $updated_rows = $this->db->execute(
                'UPDATE ' . SPBC_TBL_SCAN_FILES
                . ' SET
                checked_heuristic = 1,
                checked_signatures = 1,
                status   =   \'DENIED_BY_CT\',
                severity =   \'CRITICAL\',
                weak_spots = \'{"DENIED_HASH":{"1":["denied_hash"]}}\'
                WHERE full_hash IN (\'' . $where . '\')
                AND status <> \'QUARANTINED\';'
            );

            $stage_data_obj->set('count_denied_hashes', count($result));
            $stage_data_obj->set('count_denied_hashes_in_db', $updated_rows);
            $scanning_stages_storage->saveToDb();
        }

        $out = array(
            'end'       => 1,
            'processed' => empty($result['error']) ? count($result) : 0,
        );

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        // accordion interactivity
        $warnings_after_iteration = spbc_scanner_get_files_by_category('critical');
        $has_changes = $warnings_after_iteration !== $warnings_before_iteration;
        $refresh_data = array(
            'do_refresh' => $has_changes,
            'control_tab' => 'critical',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $out;
    }

    /**
     * Getting remote hashes of approved files
     *
     * @return array
     */
    public function get_approved_hashes() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $warnings_before_iteration = spbc_scanner_get_files_by_category('critical');

        $result = \CleantalkSP\SpbctWP\Scanner\Helper::getHashesForFiles('wordpress', 'approved', '1.0.0');

        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(GetApprovedHashes::class);

        if ( empty($result['error']) ) {
            $spbc->data['scanner']['checksums_count_ct'] = count($result);
            $spbc->save('data');

            $where = implode('\',\'', $result);

            if ( ! preg_match('#^[a-zA-Z0-9\',]+$#', $where) ) {
                return array('error' => 'BAD_PARAMS');
            }

            /**
             * Set unknown status if the file was approved before, but its has does not
             * persist in the approved hashes list for now. This implemented for cases where we
             * do change hashes list on mistakenly accepted wrong verdict
             */
            $this->db->execute(
                'UPDATE ' . SPBC_TBL_SCAN_FILES
                . ' SET
                checked_heuristic = 0,
                checked_signatures = 0,
                severity =  NULL,
                status = \'UNKNOWN\'
                WHERE status   =   \'APPROVED_BY_CT\'
                AND full_hash NOT IN (\'' . $where . '\');'
            );

            /**
             * Update the db depend on current hashes list.
             */
            $updated_rows = $this->db->execute(
                'UPDATE ' . SPBC_TBL_SCAN_FILES
                . ' SET
                checked_heuristic = 1,
                checked_signatures = 1,
                status   =   \'APPROVED_BY_CT\',
                severity =   NULL
                WHERE full_hash IN (\'' . $where . '\');'
            );

            $stage_data_obj->set('count_approved_hashes', count($result));
            $stage_data_obj->set('count_approved_hashes_in_db', $updated_rows);
            $scanning_stages_storage->saveToDb();
        }

        $out = array(
            'end'       => 1,
            'processed' => empty($result['error']) ? count($result) : 0
        );

        unset($result);

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        // accordion interactivity
        $warnings_after_iteration = spbc_scanner_get_files_by_category('critical');
        $has_changes = $warnings_after_iteration !== $warnings_before_iteration;
        $refresh_data = array(
            'do_refresh' => $has_changes,
            'control_tab' => 'critical',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $out;
    }

    /**
     * @param string $status
     * @param int $offset
     * @param int $amount
     *
     * @return array
     */
    public function signature_analysis($status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR', $offset = null, $amount = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $status_raw = isset($status) ? $status : stripslashes(Request::get('status'));
        if ( ! preg_match('#^[A-Z,_]+$#', $status_raw) ) {
            return array('error' => 'BAD_PARAMS');
        }
        $offset = isset($offset) ? $offset : $this->offset;
        $amount = isset($amount) ? $amount : $this->amount;
        $status = is_string($status_raw) ? explode(',', $status_raw) : $status_raw;
        $status = '"' . implode('","', $status) . '"';
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(SignatureAnalysis::class);

        $critical_files_found = 0;

        // Count total files if offset is 0
        if ( $offset === 0 ) {
            // There are no signatures in the DB
            $there_are_signatures_in_db = SignatureAnalysisFacade::thereAreSignaturesInDb();
            if (!$there_are_signatures_in_db) {
                // Adding to log
                ScanningLogFacade::writeToLog(
                    '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescriptionEmptySignaturesTable()
                );

                return array(
                    'found'     => 0,
                    'processed' => 0,
                    'end'       => 1,
                    'stage_data_for_logging' => array(
                        'title' => $stage_data_obj::getTitle(),
                        'description' => $stage_data_obj->getDescriptionEmptySignaturesTable()
                    )
                );
            }
            $spbc->data['scanner']['scanned_total'] = 0;
            $spbc->data['scanner']['signatures_found'] = []; // Clearing ids of the signatures found
            $total = $this->countFilesByStatusAndChecked($status_raw, 'SIGNATURE_ANALYSIS');
            if ( ! isset($total['total']) ) {
                error_log('countFilesByStatusAndChecked: ' . $total['error'] . ' ' . $total['comment']);
            }
            $total = $total['total'];
            $stage_data_obj->set('total_count_files_for_analysis', $total);

            if (isset($spbc->errors['scanner_update_signatures_bad_signatures'])) {
                // Adding to log
                ScanningLogFacade::writeToLog(
                    '<b>'
                    . $stage_data_obj::getTitle()
                    . '</b> '
                    . __('Some signatures were not recorded in the database: ', 'security-malware-firewall')
                    . $spbc->errors['scanner_update_signatures_bad_signatures']
                );
            }
        }

        // Get files to check for this iteration
        $files = $this->db->fetchAll(
            'SELECT path, source_type, source, version, status, checked_heuristic, checked_signatures, fast_hash, real_full_hash, full_hash, weak_spots, difference, severity, size, error_msg'
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . " WHERE checked_signatures = 0 AND status IN ($status)"
            . " LIMIT 1000"
        );

        $aggregated_size = 0;
        $files_to_check  = array();
        $size_breaking_flag = false;
        foreach ( $files as $file ) {
            if ( $aggregated_size < static::$max_queue_files_aggregated_size__signatures ) {
                //even if a single file over limit to load, it will be added to files for checking anyway
                $aggregated_size  += $file['size'];
                $files_to_check[] = $file;
            } else {
                $size_breaking_flag = true;
                break;
            }
        }

        // Preparing data for log
        $processed_items = array();
        foreach ( $files_to_check as $file ) {
            $processed_items[$file['fast_hash']] = array(
                'path'   => $file['path'],
                'module' => $stage_data_obj::getTitle(),
                'status' => 0,
            );
        }

        $scanned = 0;
        $statuses = new ScannerFileStatuses();

        if ( count($files_to_check) ) {
            $root_path  = spbc_get_root_path();
            $signatures = $this->db->fetchAll('SELECT * FROM ' . SPBC_TBL_SCAN_SIGNATURES);

            $signatures_scanner = new \CleantalkSP\Common\Scanner\SignaturesAnalyser\Controller();

            foreach ( $files_to_check as $file ) {
                $file_to_check = new \CleantalkSP\Common\Scanner\SignaturesAnalyser\Structures\FileInfo(
                    $file['path'],
                    $file['full_hash']
                );

                $result = $signatures_scanner->scanFile($file_to_check, $root_path, $signatures);

                $processed_items[$file['fast_hash']]['status'] = ! empty($file['status']) && $file['status'] === 'MODIFIED'
                    ? 'MODIFIED'
                    : $result->status;

                $status     = ! empty($file['status']) && $file['status'] === 'MODIFIED' ? 'MODIFIED' : $result->status;
                $weak_spots = ! empty($result->weak_spots) ? json_encode($result->weak_spots) : 'NULL';
                $severity   = ! empty($file['severity']) ? $file['severity'] : $result->severity;
                $statuses->addStatus($status);

                if ( $weak_spots !== 'NULL' ) {
                    if ($result->status === 'INFECTED') {
                        $critical_files_found += 1;
                    }
                    // Collect signatures triggered counts
                    if ( isset($result->weak_spots['SIGNATURES']) && is_array($result->weak_spots['SIGNATURES']) ) {
                        foreach ( $result->weak_spots['SIGNATURES'] as $signature_ids ) {
                            $signature_idx = $spbc->data['scanner']['signatures_found'];
                            foreach ( $signature_ids as $signature_id ) {
                                $signature_idx[$signature_id] = ! empty($signature_idx[$signature_id])
                                    ? (int)$signature_idx[$signature_id] + 1
                                    : 1;
                            }
                            $spbc->data['scanner']['signatures_found'] = $signature_idx;
                        }
                    }
                }

                $current_error_msg = self::handleErrorMsg($file['error_msg'], $result->error_msg, 'signature_analysis');

                $error_msg = !empty($current_error_msg)
                    ? '\'' . $current_error_msg . '\''
                    : 'NULL';

                $result_db = $this->db->execute(
                    'UPDATE ' . SPBC_TBL_SCAN_FILES
                    . ' SET'
                    . ' checked_signatures = 1,'
                    . ' last_sent = null,'
                    . ' status =   \'' . $status . '\','
                    . ' severity = ' . QueueHelper::prepareParamForSQLQuery($severity) . ','
                    . ' weak_spots = ' . QueueHelper::prepareParamForSQLQuery($weak_spots) . ','
                    . ' error_msg = ' . $error_msg
                    . ' WHERE fast_hash = \'' . $file['fast_hash'] . '\';'
                );

                // Adding to log
                ScanningLogFacade::writeToLog(
                    $file['path'] . ': ' . $stage_data_obj::getTitle() . ' - ' . $status
                );

                $result_db !== null ? $scanned++ : $scanned;
            }
        }

        $spbc->data['scanner']['scanned_total'] += $scanned;
        $spbc->save('data');

        $out = array(
            'found'     => count($files_to_check),
            'processed' => (int)$scanned,
            'end'       => (int)$scanned < $amount && !$size_breaking_flag,
        );

        $stage_data_obj->increase('count_files_to_check', count($files_to_check));
        $stage_data_obj->increase('scanned_count_files', $scanned);
        $stage_data_obj->merge('statuses', $statuses->getStatuses());

        // Adding data for user log
        $out['processed_items'] = $processed_items;

        if ( isset($total) ) {
            $out['total'] = $total;
        }

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $log_description = $stage_data_obj->getDescription();
        if (isset($spbc->errors['scanner_update_signatures_bad_signatures']) && $offset === 0) {
            $log_description .= ' ' . __('Some signatures were not recorded in the database: ', 'security-malware-firewall')
                                . $spbc->errors['scanner_update_signatures_bad_signatures']['error'];
        }
        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $log_description
        );

        // accordion interactivity
        $refresh_data = array(
            'do_refresh' => !empty($critical_files_found),
            'control_tab' => 'critical',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $out;
    }

    /**
     * Iterative function
     * Scan files from the DB via heuristic analysis
     *
     * @param string $status
     * @param int $offset
     * @param int $amount
     *
     * @return array
     */
    public function heuristic_analysis($status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR', $offset = null, $amount = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;
        $status_raw = isset($status) ? $status : stripslashes(Request::get('status'));
        if ( ! preg_match('#^[A-Z,_]+$#', $status_raw) ) {
            return array('error' => 'BAD_PARAMS');
        }
        $offset = isset($offset) ? $offset : $this->offset;
        $amount = isset($amount) ? $amount : $this->amount;
        $status = is_string($status_raw) ? explode(',', $status_raw) : $status_raw;
        $status = '"' . implode('","', $status) . '"';
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(HeuristicAnalysis::class);

        $suspicious_files_found = 0;

        // Count total files if offset is 0
        if ( $offset === 0 ) {
            $total = $this->countFilesByStatusAndChecked($status_raw, 'HEURISTIC_ANALYSIS');
            if ( ! isset($total['total']) ) {
                error_log('countFilesByStatusAndChecked() ERROR: ' . $total['error'] . ' ' . $total['comment']);
            }
            $total = $total['total'];
            $stage_data_obj->set('total_count_files_for_analysis', $total);
        }

        // Get files to check for this iteration
        $files = $this->db->fetchAll(
            'SELECT path, source_type, source, version, status, checked_heuristic, checked_signatures, fast_hash, real_full_hash, full_hash, weak_spots, difference, severity, size, error_msg'
            . ' FROM ' . SPBC_TBL_SCAN_FILES
            . " WHERE checked_heuristic = 0 AND status IN ($status)"
            . " LIMIT 1000"
        );

        $aggregated_size = 0;
        $files_to_check  = array();
        $size_breaking_flag = false;
        foreach ( $files as $file ) {
            if ( $aggregated_size < static::$max_queue_files_aggregated_size__heuristic ) {
                //even if a single file over limit to load, it will be added to files for checking anyway
                $aggregated_size  += $file['size'];
                $files_to_check[] = $file;
            } else {
                $size_breaking_flag = true;
                break;
            }
        }

        // Preparing data for log
        $processed_items = array();
        foreach ( $files_to_check as $file ) {
            $processed_items[$file['fast_hash']] = array(
                'path'   => $file['path'],
                'module' => $stage_data_obj::getTitle(),
                'status' => 0,
            );
        }

        $scanned = 0;
        $statuses = new ScannerFileStatuses();

        if ( count($files_to_check) ) {
            $root_path  = spbc_get_root_path();

            $heuristic_scanner = new \CleantalkSP\Common\Scanner\HeuristicAnalyser\Controller();

            foreach ( $files_to_check as $file ) {
                $file_to_check = new FileInfoExtended($file);

                $result = $heuristic_scanner->scanFile($file_to_check, $root_path);

                if (empty($result->error_msg) && $result->status === 'INFECTED') {
                    $suspicious_files_found++;
                }

                // Add log data
                $processed_items[$file['fast_hash']]['status'] = $file['status'] === 'MODIFIED'
                    ? 'MODIFIED'
                    : $result->status;

                // Insert found bad includes in table
                foreach ( $result->includes as $include ) {
                    if ( $include['status'] === false && $include['exists'] && $include['path'] ) {
                        unset($include['include']);

                        // Cutting file's path, leave path from CMS ROOT to file
                        $real_path = $include['path'];
                        $path      = str_replace($this->root, '', $real_path);
                        $path      = $spbc->is_windows ? str_replace('/', '\\', $path) : $path;
                        $mtime     = @filemtime($real_path);
                        if ( empty($mtime) ) {
                            clearstatcache($real_path);
                            $mtime = @filemtime($real_path);
                            if ( empty($mtime) ) {
                                clearstatcache($real_path);
                                $mtime = @filemtime($path);
                                if ( empty($mtime) ) {
                                    $mtime = @filectime($real_path);
                                    if ( empty($mtime) ) {
                                        $mtime = time();
                                    }
                                }
                            }
                        }
                        $size      = filesize($real_path);
                        $perms     = substr(decoct(fileperms($real_path)), 3);
                        $fast_hash = md5($path);
                        $full_hash = is_readable($real_path)
                            ? md5_file($real_path)
                            : 'unknown';

                        $this->db->prepare(
                            'INSERT INTO ' . SPBC_TBL_SCAN_FILES
                            . ' (`path`, `size`, `perms`, `mtime`,`status`,`fast_hash`, `full_hash`, `detected_at`) VALUES'
                            . "(%s, %d, %d, %d, 'UNKNOWN', %s, %s, %d)"
                            . 'ON DUPLICATE KEY UPDATE
                                size = VALUES(`size`)',
                            //should be offset to use in date()
                            array($path, $size, $perms, $mtime, $fast_hash, $full_hash, current_time('timestamp'))
                        )
                                 ->execute();

                        // Make 'processed' counter big enough to make an another iteration with new files
                        $scanned = 5;
                    }
                }

                $current_error_msg = self::handleErrorMsg($file['error_msg'], $result->error_msg, 'heuristic_analysis');

                $error_msg = !empty($current_error_msg)
                    ? '\'' . $current_error_msg . '\''
                    : 'NULL';

                $result_db = $this->db->execute(
                    'UPDATE ' . SPBC_TBL_SCAN_FILES
                    . ' SET '
                    . ' checked_heuristic = 1,'
                    . ' status = \'' . ($file['status'] === 'MODIFIED' ? 'MODIFIED' : $result->status) . '\','
                    . ' severity = ' . ($file['severity'] ? '\'' . $file['severity'] . '\'' : ($result->severity ? '\'' . $result->severity . '\'' : 'NULL')) . ','
                    . ' weak_spots = ' . ($result->weak_spots
                        ? QueueHelper::prepareParamForSQLQuery(json_encode($result->weak_spots))
                        : 'NULL')
                    . ','
                    . ' error_msg = ' . $error_msg
                    . ' WHERE fast_hash = \'' . $file['fast_hash'] . '\';'
                );

                $statuses->addStatus($file['status'] === 'MODIFIED' ? 'MODIFIED' : $result->status);

                // Adding to log
                ScanningLogFacade::writeToLog(
                    $file['path'] . ': ' . $stage_data_obj::getTitle() . ' - ' . ($file['status'] === 'MODIFIED' ? 'MODIFIED' : $result->status)
                );

                $result_db !== null ? $scanned++ : $scanned;
            }
        }

        $out = array(
            'found'     => count($files_to_check),
            'processed' => (int)$scanned,
            'end'       => (int)$scanned < $amount && !$size_breaking_flag,
        );

        // Adding data for user log
        if ( $processed_items ) {
            $out['processed_items'] = $processed_items;
        }

        if ( isset($total) ) {
            $out['total'] = $total;
        }

        $stage_data_obj->increase('count_files_to_check', count($files_to_check));
        $stage_data_obj->increase('scanned_count_files', $scanned);
        $stage_data_obj->merge('statuses', $statuses->getStatuses());

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $out['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        // accordion interactivity
        $refresh_data = array(
            'do_refresh' => !empty($suspicious_files_found),
            'control_tab' => 'suspicious',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $out;
    }

    /**
     * Schedule files to send for analysis during scan process via cron job.
     * @return array
     */
    public function schedule_send_heuristic_suspicious_files() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        // Init logging
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(ScheduleSendHeuristicSuspiciousFiles::class);

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        // Update records with pscan_pending_queue = 1, this will be checked by cron and file will be sent if so.
        $result_db = $this->db->execute(
            'UPDATE ' . SPBC_TBL_SCAN_FILES
            . ' SET '
            . ' pscan_pending_queue = 1 '
            . ' WHERE severity = "SUSPICIOUS" '
            . ' AND last_sent is NULL '
            . ' AND checked_heuristic = 1 '
            . ' AND weak_spots NOT LIKE "%SIGNATURES%" '
            . ' AND status NOT IN ("APPROVED_BY_USER", "APPROVED_BY_CT", "APPROVED_BY_CLOUD")'
            . ' AND (pscan_pending_queue IS NULL OR pscan_pending_queue = 0); '
        );

        $count = (int)$result_db;
        // Add cron job, ignore rescanning to reduce site load
        if ($count !== 0) {
            \CleantalkSP\SpbctWP\Cron::updateTask(
                'scanner_resend_pscan_files',
                'spbc_scanner_resend_pscan_files',
                SPBC_PSCAN_RESEND_FILES_STATUS_PERIOD,
                time() + SPBC_PSCAN_RESEND_FILES_STATUS_PERIOD,
                array('do_rescan' => false)
            );
            \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 * 2)
            );
        }

        $stage_data_obj->increase('count_scheduled', $count);
        // Save logging
        $scanning_stages_storage->saveToDb();

        return array(
                'processed' => $stage_data_obj->getData()['count_scheduled'],
                'scheduled'     => $stage_data_obj->getData()['count_scheduled'],
                'end'       => true,
                'message'   => __(
                    'Files sending scheduled',
                    'security-malware-firewall'
                ),
                'stage_data_for_logging' => array(
                    'title' => $stage_data_obj::getTitle(),
                    'description' => $stage_data_obj->getDescription()
                )
            );
    }

    public function auto_cure_backup() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;
        if ( $spbc->data['license_trial'] == 1 ) {
            return array('success' => true, 'end' => 1);
        }
        return spbc_backup__files_with_signatures(true);
    }

    /**
     * Run cure stage.
     * @param int $offset
     * @param int $amount
     * @return array Prepared data for AJAX call response.
     */
    public function auto_cure($offset = null, $amount = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;
        $auto_cure_enabled = isset($spbc->settings['scanner__auto_cure']) && $spbc->settings['scanner__auto_cure'] == 1;
        if ( $spbc->data['license_trial'] == 1 || !$auto_cure_enabled) {
            return [
                'processed' => 0,
                'cured' => 0,
                'end' => 1
            ];
        }

        $amount = isset($amount) ? $amount : $this->amount;
        $offset = isset($offset) ? $offset : $this->offset;

        $cure_stage = new CureStage($this->db);
        $cure_stage->runStage($offset, $amount);

        //return prepared AJAX output
        return $cure_stage->getStageResult();
    }

    /**
     * @param null $offset
     * @param null $amount
     *
     * @return array
     * @global  State $spbc
     *
     */
    public function outbound_links($offset = null, $amount = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $offset = isset($offset) ? $offset : $this->offset;
        $amount = isset($amount) ? $amount : $this->amount;
        $output = [];
        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(OutboundLinks::class);

        // Count total links
        if ( $offset === 0 ) {
            $links_scanner   = new Links(array('count' => true));
            $output['total'] = $links_scanner->posts_total;
            $stage_data_obj->set('total', $output['total']);
        }

        $scanner = new Links(
            array(
                'amount'        => $amount,
                'check_default' => false,
                'mirrors'       => ! empty($spbc->settings['scanner__outbound_links_mirrors']) ? $spbc->settings['scanner__outbound_links_mirrors'] : '',
            )
        );

        if ( ! empty($scanner->links) ) {
            //Getting current scan_id
            $scan_id = $this->db->fetch('SELECT MAX(scan_id) AS scan_num FROM ' . SPBC_TBL_SCAN_LINKS . ';');
            $scan_id = $scan_id->scan_num + 1;

            // Preparing request
            $sql_hat =
                'INSERT INTO ' . SPBC_TBL_SCAN_LINKS
                . ' (`scan_id`, `link`, `domain`, `link_text`, `page_url`)'
                . ' VALUES ';

            // Preparing data
            $links = QueueHelper::prepareParamForSQLQuery($scanner->links);
            $sql_values = array();
            foreach ( $links as $link_details ) {
                $sql_values[] = "($scan_id, {$link_details['link']}, {$link_details['domain']}, {$link_details['link_text']}, {$link_details['page_url']})";
            }
            $sql_values = implode(',', $sql_values);
            // Adding results to storage table
            $this->db->execute($sql_hat . $sql_values);
        }

        $output['found']     = $scanner->links_found;
        $output['processed'] = $scanner->posts_checked;
        $output['end']       = $scanner->posts_checked < $amount;

        $stage_data_obj->increase('founded', $output['found']);

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $output['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        // accordion interactivity
        $refresh_data = array(
            'do_refresh' => !empty($scanner->links_found),
            'control_tab' => 'outbound_links',
        );
        $output['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $output;
    }

    public function frontend_analysis($offset = null, $amount = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        global $spbc;

        $offset    = isset($offset) ? $offset : $this->offset;
        $amount    = isset($amount) ? $amount : $this->amount;
        $output    = [];
        $last_scan = date('Y-m-d H:i:s');
        $bad_pages_count = 0;
        if (isset($spbc->data['scanner']['last_scan__front_end'])) {
            $last_scan = date('Y-m-d H:i:s', $spbc->data['scanner']['last_scan__front_end']);
        } else {
            $spbc->data['scanner']['last_scan__front_end'] = time();
            $spbc->data['scanner']['first_scan__front_end'] = 1;
            $spbc->save('data');
        }

        $scanning_stages_storage = new ScanningStagesStorage();
        $scanning_stages_storage->converter->loadCollection();
        $stage_data_obj = $scanning_stages_storage->getStage(FrontendAnalysis::class);

        // Count total
        if ( $offset === 0 ) {
            $output['total']                             = Frontend::countUncheckedPages($last_scan);
            $spbc->data['scanner']['scanned_site_pages'] = 0;
            $spbc->data['scanner']['total_site_pages']   = Frontend::getTotalPages();
            $stage_data_obj->set('total', $output['total']);
            $stage_data_obj->set('total_site_pages', $spbc->data['scanner']['total_site_pages']);
        }

        // Skip scan if the \DOMDocument not exists
        if ( ! class_exists('\DOMDocument') ) {
            return array('end' => true,);
        }

        $front_scanner = new Frontend(
            array(
                'amount'             => $amount,
                'last_scan'          => $last_scan,
                'signatures'         => $this->db->fetchAll('SELECT * FROM ' . SPBC_TBL_SCAN_SIGNATURES),
                'domains_exceptions' => CSV::parseNSV(
                    $spbc->settings['scanner__frontend_analysis__domains_exclusions']
                ),
                'csrf_check'         => $spbc->settings['scanner__frontend_analysis__csrf'],
            )
        );

        $sql_hat =
            'INSERT INTO ' . SPBC_TBL_SCAN_FRONTEND
            . ' (`page_id`, `url`, `dbd_found`, `redirect_found`, `signature`, `csrf`, `bad_code`, `weak_spots`)'
            . ' VALUES ';

        foreach ( $front_scanner->pages as $page ) {
            if ( $page['bad'] ) {
                $bad_pages_count++;
                $guid       = QueueHelper::prepareParamForSQLQuery($page['guid']);
                $id         = QueueHelper::prepareParamForSQLQuery($page['ID']);
                $weak_spots = $page['found']['weak_spots'] ? QueueHelper::prepareParamForSQLQuery(
                    $page['found']['weak_spots']
                ) : 'NULL';

                // Preparing data
                $sql_values[] = "({$id}, {$guid}, {$page['found']['dbd']}, {$page['found']['redirects']}, {$page['found']['signatures']}, {$page['found']['csrf']}, NULL, {$weak_spots})";
            }
        }

        $sql_suffix =
            ' ON DUPLICATE KEY'
            . ' UPDATE'
            . ' url            = VALUES(url),'
            . ' dbd_found      = VALUES(dbd_found),'
            . ' redirect_found = VALUES(redirect_found),'
            . ' signature      = NULL,'
            . ' bad_code       = NULL,'
            . ' weak_spots	   = VALUES(weak_spots);';

        // Adding results to storage table
        $success = isset($sql_values)
            ? $this->db->execute($sql_hat . implode(',', $sql_values) . $sql_suffix)
            : true;

        /**
         * Switching the status of the first scan
         */
        if ($front_scanner->posts_count < $amount) {
            $spbc->data['scanner']['first_scan__front_end'] = 0;
        }
        $output['success']                           = $success;
        $output['processed']                         = $front_scanner->posts_count;
        $output['end']                               = $front_scanner->posts_count < $amount;
        if (array_key_exists('scanned_site_pages', $spbc->data['scanner'])) {
            $spbc->data['scanner']['scanned_site_pages'] += $output['processed'];
        }
        $spbc->save('data');

        $stage_data_obj->increase('success', $output['success']);
        $stage_data_obj->increase('processed', $output['processed']);

        $scanning_stages_storage->saveToDb();

        // Adding to log
        ScanningLogFacade::writeToLog(
            '<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
        );

        $output['stage_data_for_logging'] = array(
            'title' => $stage_data_obj::getTitle(),
            'description' => $stage_data_obj->getDescription()
        );

        // accordion interactivity
        $refresh_data = array(
            'do_refresh' => !empty($bad_pages_count),
            'control_tab' => 'frontend_malware',
        );
        $output['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $output;
    }

    public function important_files_listing() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        $out = array(
            'processed'          => 0,
            'accessible_urls'    => array(),
            'accessible_listing' => array(),
        );

        $addresses_to_check_accessibility = array(
            '/wp-content/debug.log',
            '/.svn/entries',
            '/.git/config',
        );

        $addresses_to_check_listing = array(
            '/.svn',
            '/.git',
        );

        foreach ( $addresses_to_check_accessibility as $address ) {
            $url_to_check = get_option('home') . $address;
            if ( HTTP::getResponseCode($url_to_check, true) === 200 ) {
                $out['accessible_urls'][] = array('url' => $address, 'type' => 'accessible');
            }
        }

        foreach ( $addresses_to_check_listing as $address ) {
            $url_to_check = get_option('home') . $address;
            $page = HTTP::getContentFromURL($url_to_check);
            if (
                empty($page['error']) &&
                is_string($page) &&
                strpos($page, 'Index of ' . $address) !== false
            ) {
                $out['accessible_urls'][] = array('url' => $address, 'type' => 'listing');
            }
        }

        $out['processed'] = count($addresses_to_check_accessibility) + count($addresses_to_check_listing);
        $out['end']       = 1;

        // Saving the result
        global $spbc;

        /** Fixed for PHP 8.1: PHP Deprecated:  Automatic conversion of false to array is deprecated */
        if ( ! is_array($spbc->scanner_listing) ) {
            $spbc->scanner_listing = array(
                'accessible_urls' => $out['accessible_urls']
            );
        } else {
            $spbc->scanner_listing['accessible_urls'] = $out['accessible_urls'];
        }

        $spbc->save('scanner_listing', true, false);

        $refresh_data = array(
            'do_refresh' => !empty($spbc->scanner_listing['accessible_urls']),
            'control_tab' => 'unsafe_permissions',
        );
        $out['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

        return $out;
    }

    /**
     * Send results stage
     */
    public function send_results() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        $results = new SendResultsStage();

        return $results->execute();
    }

    /**
     * Delete files which paths are included to the scanner exclusions directories.
     * @param string $scanner__dir_exclusions
     * @return bool|int
     * @psalm-suppress PossiblyUnusedReturnValue
     */
    public function deleteFilesOfExclusionDirs($scanner__dir_exclusions)
    {
        $deleted_count = 0;
        foreach ( spbc__get_exists_directories(explode("\n", $scanner__dir_exclusions)) as $exclusion ) {
            if ( $exclusion ) {
                $result = $this->db->prepare(
                    'DELETE FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE LOWER(path) LIKE LOWER(%s)',
                    ['%' . $this->db->escapeLike($exclusion) . '%']
                )->execute();
                $deleted_count = !empty($result) ? $deleted_count + (int)$result : $deleted_count;
            }
        }
        return $deleted_count;
    }

    /**
     * @param $file_record_error - current record error
     * @param $new_error_msg - new error from scan stage
     * @param $type - type of error source
     * @return string - JSON encoded string
     */
    private static function handleErrorMsg($file_record_error, $new_error_msg, $type)
    {
        if (!in_array($type, ['signature_analysis', 'heuristic_analysis'])) {
            return '';
        }
        if (is_null($file_record_error)) {
            return '';
        }
        $current_error_msg = json_decode($file_record_error, true);

        if (!empty($current_error_msg)) {
            if (!empty($new_error_msg)) {
                $current_error_msg[$type] = $new_error_msg;
            } else {
                unset($current_error_msg[$type]);
            }
        } else {
            if (!empty($new_error_msg)) {
                $current_error_msg[$type] = $new_error_msg;
            }
        }

        $current_error_msg = json_encode($current_error_msg);

        if (!empty($current_error_msg)) {
            return $current_error_msg;
        }

        return '';
    }
}
