<?php

/*
Plugin Name: Security by CleanTalk
Plugin URI: https://wordpress.org/plugins/security-malware-firewall/
Description: Security & Malware scan by CleanTalk to protect your website from online threats and viruses. IP/Country FireWall, Web application FireWall. Detailed stats and logs to have full control.
Author: CleanTalk Security
Version: 2.151
Author URI: https://cleantalk.org
Text Domain: security-malware-firewall
Domain Path: /i18n
*/

use CleantalkSP\SpbctWP\Activator;
use CleantalkSP\SpbctWP\AdjustToEnvironmentModule\AdjustToEnvironmentHandler;
use CleantalkSP\SpbctWP\DTO\SecurityLogsDataRowDTO;
use CleantalkSP\SpbctWP\DTO\SecurityLogsDTO;
use CleantalkSP\SpbctWP\FSWatcher\Controller as FSWatcherController;
use CleantalkSP\SpbctWP\DB;
use CleantalkSP\SpbctWP\Firewall\BFP;
use CleantalkSP\SpbctWP\Firewall\FW;
use CleantalkSP\SpbctWP\Cron as SpbcCron;
use CleantalkSP\SpbctWP\HTTP\CDNHeadersChecker;
use CleantalkSP\SpbctWP\RemoteCalls as SpbcRemoteCalls;
use CleantalkSP\SpbctWP\RenameLoginPage;
use CleantalkSP\SpbctWP\Sanitize;
use CleantalkSP\SpbctWP\Scanner\Stages\SignatureAnalysis\SignatureAnalysisFacade;
use CleantalkSP\SpbctWP\State;
use CleantalkSP\SpbctWP\Transaction;
use CleantalkSP\SpbctWP\Variables\Cookie;
use CleantalkSP\SpbctWP\VulnerabilityAlarm\VulnerabilityAlarmService;
use CleantalkSP\Updater\Updater;
use CleantalkSP\Updater\UpdaterScripts;
use CleantalkSP\Variables\Get;
use CleantalkSP\Variables\Post;
use CleantalkSP\Variables\Server;
use CleantalkSP\SpbctWP\Helpers\IP;
use CleantalkSP\SpbctWP\Helpers\HTTP;
use CleantalkSP\SpbctWP\API as SpbcAPI;
use CleantalkSP\SpbctWP\Scanner\ScanRepository;
use CleantalkSP\SpbctWP\Scanner\ScanStorage;
use CleantalkSP\SpbctWP\VulnerabilityAlarm\VulnerabilityAlarm;

// Prevent direct call
if ( ! defined('WPINC') ) {
    die('Not allowed!');
}

// Getting version form main file (look above)
$plugin_info           = get_file_data(__FILE__, array('Version' => 'Version', 'Name' => 'Plugin Name', 'Description' => 'Description'));
$plugin_version__agent = $plugin_info['Version'];
// Converts xxx.xxx.xx-dev to xxx.xxx.2xx
// And xxx.xxx.xx-fix to xxx.xxx.1xx
if ( preg_match('@^(\d+)\.(\d+)\.(\d{1,2})-(dev|fix)$@', $plugin_version__agent, $m) ) {
    $plugin_version__agent = $m[1] . '.' . $m[2] . '.' . ($m[4] === 'dev' ? '2' : '1') . str_pad($m[3], 2, '0', STR_PAD_LEFT);
}

// Common params
define('SPBC_NAME', $plugin_info['Name']);
define('SPBC_VERSION', $plugin_info['Version']);
define('SPBC_AGENT', 'wordpress-security-' . $plugin_version__agent);
define('SPBC_USER_AGENT', 'Cleantalk-Security-Wordpress-Plugin/' . $plugin_info['Version']);
define('SPBC_API_URL', 'https://api.cleantalk.org');        //Api URL
define('SPBC_PLUGIN_DIR', dirname(__FILE__) . DIRECTORY_SEPARATOR); //System path. Plugin root folder with '/'.
define('SPBC_PLUGIN_BASE_NAME', plugin_basename(__FILE__)); //Plugin base name.
define(
    'SPBC_PATH',
    is_ssl()
        ? preg_replace('/^http(s)?/', 'https', plugins_url('', __FILE__))
        : plugins_url('', __FILE__)
); //HTTP(S)? path.   Plugin root folder without '/'.

// SSL Serttificate path
if ( ! defined('CLEANTALK_CASERT_PATH') ) {
    define('CLEANTALK_CASERT_PATH', file_exists(ABSPATH . WPINC . '/certificates/ca-bundle.crt') ? ABSPATH . WPINC . '/certificates/ca-bundle.crt' : '');
}

// Options names
define('SPBC_DATA', 'spbc_data');             //Option name with different plugin data.
define('SPBC_SETTINGS', 'spbc_settings');         //Option name with plugin settings.
define('SPBC_NETWORK_SETTINGS', 'spbc_network_settings'); //Option name with plugin network settings.
define('SPBC_CRON', 'spbc_cron');             //Option name with scheduled tasks.
define('SPBC_ERRORS', 'spbc_errors');           //Option name with errors.
define('SPBC_DEBUG', 'spbc_debug');            //Option name with a debug data. Empty by default.
define('SPBC_PLUGINS', 'spbc_plugins');          //Option name with a debug data. Empty by default.
define('SPBC_THEMES', 'spbc_themes');           //Option name with a debug data. Empty by default.

// Different params
define('SPBC_REMOTE_CALL_SLEEP', 10); //Minimum time between remote call
define('SPBC_LAST_ACTIONS_TO_VIEW', 20); //Nubmer of last actions to show in plugin settings page.

// Auth params
define('SPBC_2FA_KEY_TTL', 600);      // 2fa key lifetime in seconds

// DataBase params
global $wpdb;

define('SPBC_TBL_FIREWALL_DATA', $wpdb->base_prefix . 'spbc_firewall_data');
define('SPBC_TBL_FIREWALL_DATA_V4', SPBC_TBL_FIREWALL_DATA . '_v4');
define('SPBC_TBL_FIREWALL_DATA_V6', SPBC_TBL_FIREWALL_DATA . '_v6');
define('SPBC_TBL_FIREWALL_DATA__IPS', $wpdb->prefix . 'spbc_firewall__personal_ips');
define('SPBC_TBL_FIREWALL_DATA__IPS_V4', SPBC_TBL_FIREWALL_DATA__IPS . '_v4'); // Table with firewall IPS v4
define('SPBC_TBL_FIREWALL_DATA__IPS_V6', SPBC_TBL_FIREWALL_DATA__IPS . '_v6'); // Table with firewall IPS v6
define('SPBC_TBL_FIREWALL_DATA__COUNTRIES', $wpdb->prefix . 'spbc_firewall__personal_countries'); // Table with firewall countries.
define('SPBC_TBL_FIREWALL_LOG', $wpdb->prefix . 'spbc_firewall_logs'); // Table with firewall logs.
define('SPBC_TBL_SESSIONS', $wpdb->prefix . 'spbc_sessions'); // Alternative sessions table

define('SPBC_TBL_MONITORING_USERS', $wpdb->prefix . 'spbc_monitoring_users'); // Table with users monitoring data
define('SPBC_TBL_SECURITY_LOG', $wpdb->prefix . 'spbc_auth_logs');       // Table with security logs.
define('SPBC_TBL_TC_LOG', $wpdb->prefix . 'spbc_traffic_control_logs'); // Table with traffic control logs.
define('SPBC_TBL_BFP_BLOCKED', $wpdb->prefix . 'spbc_bfp_blocked'); // Table with traffic control logs.
define('SPBC_TBL_SCAN_FILES', $wpdb->base_prefix . 'spbc_scan_results');    // Table with scan results.
define('SPBC_TBL_SCAN_RESULTS_LOG', $wpdb->base_prefix . 'spbc_scan_results_log');    // Table with log of scan results.
define('SPBC_TBL_SCAN_LINKS', $wpdb->prefix . 'spbc_scan_links_logs'); // For links scanner. Results of scan.
define('SPBC_TBL_SCAN_FRONTEND', $wpdb->base_prefix . 'spbc_scan_frontend');   // For frontend scanner. Results of scan.
define('SPBC_TBL_SCAN_SIGNATURES', $wpdb->base_prefix . 'spbc_scan_signatures'); // For malware signatures.
define('SPBC_TBL_BACKUPED_FILES', $wpdb->prefix . 'spbc_backuped_files');  // Contains backuped files
define('SPBC_TBL_BACKUPS', $wpdb->prefix . 'spbc_backups');         // Contains backup info.
define('SPBC_TBL_CURE_LOG', $wpdb->base_prefix . 'spbc_cure_log');    // Table with scan results.
define('SPBC_SURFACE_COMPLETED_DIRS', $wpdb->base_prefix . 'spbc_surface_completed_dirs');    // Table with scan results.
define('SPBC_SELECT_LIMIT', 1500);                 // Select limit for logs.
define('SPBC_WRITE_LIMIT', 5000);                 // Write limit for firewall data.

// Multisite
define('SPBC_WPMS', (is_multisite() ? true : false)); // WMPS is enabled

// Scanner params for background scanning
define('SPBC_SCAN_SURFACE_AMOUNT', 1000); // Surface scan amount for 1 iteration
define('SPBC_SCAN_SURFACE_PERIOD', 30);   // Surface scan call period
define('SPBC_SCAN_MODIFIED_AMOUNT', 5);    // Deep scan amount for 1 iteration
define('SPBC_SCAN_SIGNATURE_AMOUNT', 20);    // Deep scan amount for 1 iteration
define('SPBC_SCAN_MODIFIED_PERIOD', 30);   // Deep scan call period
define('SPBC_SCAN_LINKS_AMOUNT', 10);      // Links scan amount for 1 iteration
define('SPBC_SCAN_FRONTEND_AMOUNT', 10);      // Links scan amount for 1 iteration
define('SPBC_SCAN_LINKS_PERIOD', 30);      // Links scan call period
define('SPBC_PSCAN_UPDATE_FILES_STATUS_PERIOD', 60);      // Check cloud analysis files status period
define('SPBC_PSCAN_RESEND_FILES_STATUS_PERIOD', 300);      // Resend files

// brief data limits
define('SPBC_BRIEF_DATA_DAYS_LIMIT', 7);      // how many days will be logs looked for
define('SPBC_BRIEF_DATA_ACTIONS_LIMIT', 10);      // how many actions will be logs looked for


require_once SPBC_PLUGIN_DIR . 'lib/spbc-php-patch.php'; // PHP functions patches
require_once SPBC_PLUGIN_DIR . 'lib/autoloader.php'; // Autoloader

require_once SPBC_PLUGIN_DIR . 'inc/spbc-backups.php';
require_once SPBC_PLUGIN_DIR . 'inc/fw-update.php';

// Misc libs
require_once SPBC_PLUGIN_DIR . 'inc/spbc-tools.php';   // Different helper functions
require_once SPBC_PLUGIN_DIR . 'inc/spbc-pluggable.php'; // WordPress functions
require_once SPBC_PLUGIN_DIR . 'inc/spbc-scanner.php';

// ArrayObject with settings and other global variables
global $spbc;
$spbc = new State(
    'spbc',
    array(
        'settings',
        'data',
        'remote_calls',
        'debug',
        'installing',
        'errors',
        'fw_stats',
        'scan_plugins_info',
        'scan_themes_info'
    ),
    is_multisite(),
    is_main_site()
);

require_once SPBC_PLUGIN_DIR . 'inc/spbc-auth.php';

// Update plugin's data to current version
spbc_update_actions();

// Remote calls

if ( SpbcRemoteCalls::check() ) {
    try {
        if ( Get::get('spbc_remote_call_action') === 'run_service_template_get' ) {
            require_once(SPBC_PLUGIN_DIR . 'inc/spbc-settings.php');
        }
        $rc = new SpbcRemoteCalls($spbc);
        $rc->process();
    } catch ( Exception $e ) {
        die(json_encode(array('ERROR:' => $e->getMessage())));
    }
}

//First start
if ( $spbc->settings && $spbc->key_is_ok) {
    require_once SPBC_PLUGIN_DIR . 'inc/spbc-firewall.php';

    if ( ! spbc_firewall_skip_check()) {
        if ( is_admin() && spbc_is_user_logged_in() ) {
            //do this if in admin area and user is logged in - check only admin area (WAF run)
            spbc_firewall_check_admin_area();
            if ( ! empty($_FILES) ) {
                spbc_upload_checker__check();
            }
        } else {
            //if not in admin area and user is not logged in - check with all modules
            spbc_firewall__check();
        }
    }
} elseif ( isset($spbc->errors) && ! isset($spbc->errors['apikey']) ) {
    if ($spbc->settings['spbc_key'] === '') {
        $text = __('Access key is empty.', 'security-malware-firewall');
    } else {
        $text = __('Unknown access key.', 'security-malware-firewall');
    }
    $spbc->error_add('apikey', $text);
}

// Disable XMLRPC if setting is enabled
if ( $spbc->settings['wp__disable_xmlrpc'] ) {
    add_filter('xmlrpc_enabled', '__return_false');
}

// Disable WordPress REST API for non-authenticated
if ( $spbc->settings['wp__disable_rest_api_for_non_authenticated'] ) {
    add_filter(
        'rest_authentication_errors',
        function ($result) {
            if ( empty($result) && ! is_user_logged_in() ) {
                return new WP_Error(
                    'rest_not_logged_in',
                    'You are not currently logged in.',
                    array('status' => 401)
                );
            }
            return $result;
        }
    );
}

// Disable the WordPress endpoint "users" REST API
add_filter('rest_authentication_errors', 'spbc_rest_authentication_errors');

function spbc_rest_authentication_errors($result)
{
    global $spbc;

    if (
        $spbc->settings['wp__disable_rest_api_route_users'] &&
        Server::inUri('/wp/') &&
        Server::inUri('users')
    ) {
        return new WP_Error(
            'access_denied',
            __('Access is closed (Security by CleanTalk)'),
            array( 'status' => 401 )
        );
    }
    return $result;
}

if ( ! is_admin() && $spbc->settings['misc__prevent_logins_collecting'] ) {
    add_filter('redirect_canonical', 'spbc_redirect_to_honeypot_login', 1, 2);
}

/**
 * This is the Cron handler for the `spbc_security_check_vulnerabilities` task
 *
 * @return array|void
 */
function spbc_security_check_vulnerabilities()
{
    global $spbc;
    try {
        VulnerabilityAlarm::updateWPModulesVulnerabilities();
        $spbc->data['spbc_security_check_vulnerabilities_last_call'] = time();
        $spbc->save('data');
        // Send found vulnerabilities to the cloud
        VulnerabilityAlarmService::sendReport();
    } catch ( \Exception $exception ) {
        return ['error' => $exception->getMessage()];
    }
}

function spbc_update_scan_settings_exclusions()
{
    global $spbc;

    $settings = $spbc->settings;

    try {
        $dirExclusion = new \CleantalkSP\SpbctWP\Settings\FilesScanDirExclusion();

        $settings['scanner__dir_exclusions_view'] = $dirExclusion->dirExclusionsView($settings['scanner__dir_exclusions_view']);
        $settings['scanner__dir_exclusions'] = $dirExclusion->dirExclusions($settings['scanner__dir_exclusions_view']);

        $domainExclusion = new \CleantalkSP\SpbctWP\Settings\FrontendScanDomainExclusion();

        $domainExclusionView = $domainExclusion->frontendScanDomainExclusionsView($settings['scanner__frontend_analysis__domains_exclusions_view']);
        $settings['scanner__frontend_analysis__domains_exclusions_view'] = $domainExclusionView;

        $domainExclusionSets = $domainExclusion->domainExclusions($settings['scanner__frontend_analysis__domains_exclusions_view']);
        $settings['scanner__frontend_analysis__domains_exclusions'] = $domainExclusionSets;

        $domainExclusion->resetScannerFrontendResult($settings);

        $spbc->settings = $settings;
        $spbc->save('settings');
    } catch ( \Exception $exception ) {
        return ['error' => $exception->getMessage()];
    }
}

function spbc_redirect_to_honeypot_login($redirect, $request)
{
    if ( preg_match('/author=\d+/i', $request) ) {
        add_filter('author_link', 'spbc_change_author_name', 10, 3);
    }

    return $redirect;
}

function spbc_change_author_name($link, $_author_id, $_author_nicename)
{
    $link = preg_replace('@(.*?)([\w-]+\/)$@', '$1honeypot_login_' . microtime(true), $link);
    wp_redirect($link);
    die();
}

if ( $spbc->settings['monitoring__users'] ) {
    add_action('admin_head', array( '\CleantalkSP\Monitoring\User', 'record' ));
    add_action('wp_head', array( '\CleantalkSP\Monitoring\User', 'record' ));
}

//Password-protected pages also uses wp-login page, we should not break it
if ( $spbc->settings['login_page_rename__enabled'] ) {
    if ( Get::get('action') === 'postpass' ) {
        require ABSPATH . 'wp-includes/pluggable.php';
        require ABSPATH . 'wp-login.php';
    }

    new RenameLoginPage(
        $spbc->settings['login_page_rename__name'],
        $spbc->settings['login_page_rename__redirect']
    );
}

// Logged hooks
register_activation_hook(__FILE__, 'spbc_activation');
register_deactivation_hook(__FILE__, 'spbc_deactivation');
register_uninstall_hook(__FILE__, 'spbc_uninstall');

// Hook for newly added blog
Activator::addActionForNetworkBlogLegacy(get_bloginfo('version'));

add_action('plugins_loaded', 'spbc_plugin_loaded', 1);     // Main hook

// Posts hooks
add_action('wp_insert_post', 'spbc_update_postmeta_links', 10, 3);
add_action('wp_insert_comment', 'spbc_update_postmeta_links__by_comment', 10, 2);

// Set headers
add_action('init', 'spbc_set_headers');

if ( $spbc->settings['spbc_trusted_and_affiliate__footer'] === '1' ) {
    add_action('wp_enqueue_scripts', 'spbc_attach_public_css');
    add_action('wp_footer', 'spbc_hook__wp_footer_trusted_text', 998);
}

// Cron
global $spbc_cron; // Letting know functions that they are running under spbc_cron
$spbc_cron = new SpbcCron();
! SpbcRemoteCalls::check() && $spbc_cron->execute();
unset($spbc_cron);

if ( is_admin() || is_network_admin() ) {
    // Async loading for JavaScript
    add_filter('script_loader_tag', 'spbc_admin_add_script_attribute', 10, 3);

    include_once SPBC_PLUGIN_DIR . 'inc/spbc-admin.php';
    include_once SPBC_PLUGIN_DIR . 'templates/spbc_settings_main.php'; // Templates for settings pgae

    add_action('admin_init', array('CleantalkSP\SpbctWP\Activator', 'redirectAfterActivation'), 1); // Redirect after activation
    add_action('admin_init', 'spbc_admin_init', 1, 1);       // Main admin hook
    add_action('admin_menu', 'spbc_admin_add_page');         // Admin pages
    add_action('network_admin_menu', 'spbc_admin_add_page');         // Network admin pages
    add_action('admin_enqueue_scripts', 'spbc_enqueue_scripts');        // Scripts

    // Getting dashboard widget statistics by click
    if ( (int) Post::get('spbc_brief_refresh') === 1 ) {
        spbc_set_brief_data();
    }

    if ( $spbc->settings['wp__dashboard_widget__show'] ) {
        add_action('wp_dashboard_setup', 'spbc_widget_scripts_init');
        add_action('wp_dashboard_setup', 'spbc_dashboard_statistics_widget');
    }


    add_action('admin_init', function () {
        global $spbc;
        $admin_banners_handler = new \CleantalkSP\SpbctWP\AdminBannersModule\AdminBannersHandler($spbc);
        $admin_banners_handler->handle();
    });

    // Customize row with the plugin on plugins list page.
    if ( ( isset($pagenow) && $pagenow === 'plugins.php' ) || ( isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'plugins.php') !== false ) ) {
        add_filter('plugin_action_links_' . SPBC_PLUGIN_BASE_NAME, 'spbc_plugin_action_links', 10, 2);
        add_filter('network_admin_plugin_action_links_' . SPBC_PLUGIN_BASE_NAME, 'spbc_plugin_action_links', 10, 2);
        add_filter('all_plugins', 'spbc_admin__change_plugin_description');
        add_filter('plugin_row_meta', 'spbc_plugin_links_meta', 10, 2);
    }
}

add_action('init', function () use ($spbc) {
    if ( $spbc->feature_restrictions->getState($spbc, 'fswatcher')->is_active && $spbc->settings['scanner__fs_watcher'] ) {
        $fswatcher_params = new \CleantalkSP\SpbctWP\FSWatcher\Dto\FSWatcherParams();
        $fswatcher_params->dir_to_watch = ABSPATH;
        $fswatcher_params->exclude_dirs = [];
        $fswatcher_params->extensions_to_watch = ['php'];
        $fswatcher = new FSWatcherController($fswatcher_params);
        $fswatcher::work();
    }
});

function spbc_set_headers()
{
    global $spbc;
    if ( ! headers_sent() ) {
        // Additional headers
        if ( $spbc->settings['data__additional_headers'] ) {
            header('X-XSS-Protection: 1; mode=block');
            header('X-Content-Type-Options: nosniff');
            header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
            header('Referrer-Policy: strict-origin-when-cross-origin');
        }

        // Forbid to show in iframes
        if ( $spbc->settings['misc__forbid_to_show_in_iframes'] ) {
            header('X-Frame-Options: sameorigin', false);
        }

        // Set cookie to detect any logged in user
        if (
            spbc_is_user_logged_in() &&
            ! empty($spbc->settings['data__set_cookies']) &&
            (
                ! Cookie::get('spbc_is_logged_in') ||
                Cookie::get('spbc_is_logged_in') !== md5($spbc->data['salt'] . get_option('home'))
            )
        ) {
            // skip rewriting spbc_is_logged_in cookie on favicon request for WPMS - its always run on main site url and returns it`s home url
            if (
                is_multisite() &&
                strpos(Server::get('REQUEST_URI'), 'favicon.ico') !== false
            ) {
                return;
            }
            //rewrite spbc_is_logged_in cookie
            Cookie::set('spbc_is_logged_in', md5($spbc->data['salt'] . get_option('home')), time() + 86400 * 365, '/');
        }
    }
}

function spbc_update_actions()
{
    global $spbc;

    //Update logic
    $current_version = $spbc->data['plugin_version'];

    if ( $current_version != SPBC_VERSION ) {
        //Migrate DB data on updating to 2.128.1
        add_action('ColumnCreator_before_drop_column_analysis_status', [UpdaterScripts::class, 'migrateDbData_2_128_1']);
        add_action('ColumnCreator_before_change_column_event', [UpdaterScripts::class, 'migrateDbData_2_141_0']);

        // Perform a transaction and exit transaction ID isn't match
        if ( ! Transaction::get('updater', 5)->perform() ) {
            return;
        }

        Updater::runUpdateScripts($current_version, SPBC_VERSION);

        $spbc->data['plugin_version'] = SPBC_VERSION;
        $spbc->save('data');

        Transaction::get('updater')->clearTransactionTimer();
    }
}

/**
 * Plugin activation
 *
 * @param $network
 * @param $redirect
 *
 * @return void
 * @throws Exception
 */
function spbc_activation($network, $redirect = true)
{
    Activator::activation($network, $redirect);
}


/**
 * A code during plugin deactivation.
 *
 * @param $network
 *
 * @return void
 */
function spbc_deactivation($network)
{
    \CleantalkSP\SpbctWP\Deactivator::deactivation($network);
}

/**
 * Run deactivation process (complete deactivation forced) for hook register_uninstall_hook.
 * @param bool $network Is network wide command.
 * @return void
 */
function spbc_uninstall($network)
{
    global $spbc;
    $spbc->settings['misc__complete_deactivation'] = 1;
    $spbc->save('settings');
    \CleantalkSP\SpbctWP\Deactivator::deactivation($network);
}

/**
 * @deprecated 2.125 use Deactivator::deleteBlogTables()
 * @return void
 */
function spbc_deactivation__delete_blog_tables() //deprecated
{
    \CleantalkSP\SpbctWP\Deactivator::deleteBlogTables();
}

/**
 * @deprecated  2.125 use Deactivator::deleteCommonTables()
 * @return void
 */
function spbc_deactivation__delete_common_tables() //deprecated
{
    \CleantalkSP\SpbctWP\Deactivator::deleteCommonTables();
}

// Misc functions to test the plugin.
function spbc_plugin_loaded()
{
    global $spbc;

    if ( is_admin() || is_network_admin() ) {
        $dir    = plugin_basename(dirname(__FILE__)) . '/i18n';
        load_plugin_textdomain('security-malware-firewall', false, $dir);
    }

    if ( $spbc->settings['spbc_trusted_and_affiliate__shortcode'] === '1' ) {
        add_action('wp_enqueue_scripts', 'spbc_attach_public_css');
        add_shortcode('cleantalk_security_affiliate_link', 'spbc_trusted_text_shortcode_handler');
    }
}

/**
 * Check brute force attack
 *
 * @return void
 */
function spbc_authenticate__check_brute_force()
{
    global $spbc;

    $login_url = wp_login_url();
    if ($spbc->settings['login_page_rename__enabled']) {
        $GLOBALS['wp_rewrite'] = new WP_Rewrite();
        $login_url = RenameLoginPage::getURL($spbc->settings['login_page_rename__name']);
    }

    $bfp = new BFP(
        array(
            'api_key'       => $spbc->api_key,
            'state'         => $spbc,
            'is_login_page' => strpos(trim(Server::getURL(), '/'), trim($login_url, '/')) === 0,
            'is_logged_in'  => Cookie::get('spbc_is_logged_in') === md5($spbc->data['salt'] . get_option('home')),
            'bf_limit'      => $spbc->settings['bfp__allowed_wrong_auths'],
            'block_period'  => $spbc->settings['bfp__block_period__5_fails'],
            'count_period'  => $spbc->settings['bfp__count_interval'],
        )
    );

    $bfp->setDb(new DB());
    $bfp->setIpArray([IP::get()]);
    $bfp_result = $bfp->check();
    $bfp->middleAction();

    if (!empty($bfp_result)) {
        $bfp->_die($bfp_result[0]);
    }
}

//
// Sorts some data.
//
function spbc_usort_desc($a, $b)
{
    return $b->datetime_ts - $a->datetime_ts;
}

/**
 * Function to get the countries by IPs list.
 *
 * @param $ips_data
 *
 * @return array
 */
function spbc_get_countries_by_ips($ips_data = '')
{
    $ips_c = array();

    if ( $ips_data === '' ) {
        return $ips_c;
    }

    $result = SpbcAPI::method__ip_info($ips_data);

    if ( empty($result['error']) ) {
        foreach ( $result as $ip_dec => $v2 ) {
            if ( isset($v2['country_code']) ) {
                $ips_c[ $ip_dec ]['country_code'] = $v2['country_code'];
            }
            if ( isset($v2['country_name']) ) {
                $ips_c[ $ip_dec ]['country_name'] = $v2['country_name'];
            }
        }
    }

    return $ips_c;
}

/**
 * Gets and write new signatures in local database
 *
 * @return bool|array
 * @global State $spbc
 * @global WPDB $wpdb
 */
function spbc_scanner__signatures_update()
{
    global $spbc;

    /**
     * @psalm-suppress InvalidScalarArgument
     */
    $spbc->error_delete('scanner_update_signatures_bad_signatures', 'save');

    $latest_signature_submitted_time = SignatureAnalysisFacade::getLatestSignatureSubmittedTime();

    $signatures_from_cloud = SignatureAnalysisFacade::getSignaturesFromCloud($latest_signature_submitted_time);

    // Signatures updated
    if (isset($signatures_from_cloud['error']) && $signatures_from_cloud['error'] === 'UP_TO_DATE') {
        return array('success' => 'UP_TO_DATE');
    }

    // There is errors
    if (isset($signatures_from_cloud['error'])) {
        return $signatures_from_cloud;
    }

    $signatures = $signatures_from_cloud['values'];
    $map        = $signatures_from_cloud['map'];

    SignatureAnalysisFacade::clearSignaturesTable();

    $signatures_added = SignatureAnalysisFacade::addSignaturesToDb($map, $signatures);

    if (!$signatures_added) {
        // Attempt to record one at a time
        $signatures_added = SignatureAnalysisFacade::addSignaturesToDbOneByOne($map, $signatures);

        if (isset($signatures_added['bad_signatures'])) {
            $spbc->error_add('scanner_update_signatures_bad_signatures', $signatures_added['bad_signatures']);
        }
    }

    $spbc->data['scanner']['last_signature_update'] = current_time('timestamp');
    $spbc->data['scanner']['signature_count']       = count($signatures);
    $spbc->save('data');

    return true;
}

/**
 * Sending Security FireWall logs
 *
 * @param $api_key
 *
 * @return array|int
 */
function spbc_send_firewall_logs($api_key = false)
{
    global $spbc;

    $api_key = ! empty($api_key) ? $api_key : $spbc->api_key;

    if ( ! empty($api_key) ) {
        $result = FW::sendLog(
            DB::getInstance(),
            SPBC_TBL_FIREWALL_LOG,
            $api_key
        );

        if ( empty($result['error']) ) {
            $spbc->fw_stats['last_send']       = current_time('timestamp');
            $spbc->fw_stats['last_send_count'] = $result;
            $spbc->save('fw_stats', true, false);

            return $result;
        }

        return $result;
    }

    return array(
        'error' => 'KEY_EMPTY'
    );
}

/**
 * Drop Security FireWall data
 *
 * @return bool|string[]
 */
function spbc_security_firewall_drop()
{
    global $wpdb;

    // @psalm-suppress WpdbUnsafeMethodsIssue
    $result = $wpdb->query('DELETE FROM `' . SPBC_TBL_FIREWALL_DATA . '`;');

    if ( $result !== false ) {
        return true;
    }

    return array( 'error' => 'DELETE_ERROR' );
}

/**
 * Handle firewall private_records remote call.
 * @param $action string 'add','delete'
 * @param $test_data string JSON string used in test cases
 * @return string JSON string of results
 * @throws Exception
 */
function spbct_sfw_private_records_handler($action, $test_data = null)
{

    $error = 'secfw_private_records_handler: ';

    if ( !empty($action) && (in_array($action, array('add', 'delete'))) ) {
        $metadata = !empty($test_data) ? $test_data : Post::get('metadata');

        /**
         * Validate JSON
         */
        if ( !empty($metadata) ) {
            $metadata = json_decode(stripslashes($metadata), true);
            if ( $metadata === 'NULL' || $metadata === null ) {
                throw new InvalidArgumentException($error . 'metadata JSON decoding failed');
            }
        } else {
            throw new InvalidArgumentException($error . 'metadata is empty');
        }

        foreach ( $metadata as $_key => &$row ) {
            $row = explode(',', $row);

            /**
             * Validation of JSON decoded array data
             */
            $ip_validated = false;
            $validation_error = '';

            //validate IP
            if ( IP::validate($row[0]) === 'v6' ) {
                $ip_validated = $row[0];
            } elseif (
                IP::validate(long2ip((int)$row[0])) === 'v4'
                && (int)($row[0]) === ip2long(long2ip((int)$row[0]))
            ) {
                $ip_validated = (int)$row[0];
            } else {
                $validation_error = 'network value does not look like IP address ';
            }

            //do this to get info more obvious
            $metadata_assoc_array = array(
                'network' => $ip_validated ?: null,
                'mask' => (int)$row[1],
                'status' => isset($row[2]) && $row[2] !== '' ? (int)$row[2] : null,
            );

            //validate mask and status
            if ( $metadata_assoc_array['mask'] === 0
                || $metadata_assoc_array['mask'] > 4294967295
            ) {
                $validation_error = 'metadata validate failed on "mask" value';
            }

            //only for adding
            if ( $action === 'add' ) {
                if ( !in_array($metadata_assoc_array['status'], array(-4, -3, -2, -1, 0, 1, 2, 99)) ) {
                    $validation_error = 'metadata validate failed on "status" value';
                }
            }

            if ( !empty($validation_error) ) {
                throw new InvalidArgumentException($error . $validation_error);
            }

            /**
             * Ip version logic
             */
            if ( is_string($metadata_assoc_array['network']) ) {
                $metadata_assoc_array['network'] = IP::convertIPv6ToFourIPv4(IP::extendIPv6(IP::normalizeIPv6($metadata_assoc_array['network'])));


                if ($metadata_assoc_array['mask'] > 128) {
                    $validation_error = 'metadata validate failed on "mask" value';
                    break;
                }

                /**
                 * Versatility for mask for v6 and v4
                 * @psalm-suppress LoopInvalidation
                 */
                for ( $masks = array(), $mask = $metadata_assoc_array['mask'], $k = 4; $k >= 1; $k-- ) {
                    $masks[$k] = (2 ** 32) - (2 ** (32 - ($mask > 32 ? 32 : $mask)));
                    $mask -= 32;
                    $mask = $mask > 0 ? $mask : 0;
                }
                $metadata_assoc_array['mask'] = $masks;
            }

            //all checks done, change on link
            $row = $metadata_assoc_array;
        }
        unset($row);

        if ( !empty($validation_error) ) {
            throw new InvalidArgumentException($error . $validation_error);
        }

        //method selection
        if ( $action === 'add' ) {
            $handler_output = FW::privateRecordsAdd(
                DB::getInstance(),
                $metadata
            );
        } elseif ( $action === 'delete' ) {
            $handler_output = FW::privateRecordsDelete(
                DB::getInstance(),
                $metadata
            );
        } else {
            $error .= 'unknown action name: ' . $action;
            throw new InvalidArgumentException($error);
        }
    } else {
        throw new InvalidArgumentException($error . 'empty action name');
    }

    return json_encode(array('OK' => $handler_output));
}

function spbc_update_postmeta_links($post_ID)
{
    delete_post_meta($post_ID, '_spbc_links_checked');
    delete_post_meta($post_ID, 'spbc_links_checked');
}

function spbc_update_postmeta_links__by_comment($id)
{
    $comment = get_comment($id);
    spbc_update_postmeta_links($comment->comment_post_ID);
}

// Install MU-plugin
function spbc_mu_plugin__install()
{

    // If WPMU_PLUGIN_DIR is not exists -> create it
    if ( ! is_dir(WPMU_PLUGIN_DIR) && ! mkdir(WPMU_PLUGIN_DIR) && ! is_dir(WPMU_PLUGIN_DIR) ) {
        throw new \RuntimeException(sprintf('Directory "%s" was not created', WPMU_PLUGIN_DIR));
    }

    // Get data from info file and write it to new plugin file
    $file = '<?php' . PHP_EOL . file_get_contents(SPBC_PLUGIN_DIR . '/install/security-malware-firewall-mu.php');

    return @file_put_contents(WPMU_PLUGIN_DIR . '/0security-malware-firewall-mu.php', $file) ? true : false;
}

/**
 * Uninstall MU-plugin
 * @deprecated 2.125 Use Deactivator::muPluginUninstall
 * @return bool
 */
function spbc_mu_plugin__uninstall()
{
    return \CleantalkSP\SpbctWP\Deactivator::muPluginUninstall();
}

function spbc_user_is_admin()
{
    global $spbc;

    if (!empty($spbc->settings['data__set_cookies'])) {
        return
            Cookie::get('spbc_is_logged_in') === md5($spbc->data['salt'] . get_option('home')) &&
            Cookie::get('spbc_admin_logged_in') === md5($spbc->data['salt'] . 'admin' . get_option('home'));
    }

    return is_admin();
}

//Function to send logs
function spbc_send_logs($api_key = null)
{
    global $spbc, $wpdb;

    if ( $api_key == null ) {
        if ( ! $spbc->is_mainsite && $spbc->ms__work_mode == 2 ) {
            $api_key = $spbc->network_settings['spbc_key'];
        } else {
            $api_key = $spbc->settings['spbc_key'];
        }
    }

    if ( ! $api_key ) {
        return array(
            'error' => 'KEY_EMPTY'
        );
    }

    $wpms_snippet = SPBC_WPMS
        ? (" WHERE blog_id = " . get_current_blog_id() . ' AND ')
        : " WHERE ";

    // @psalm-suppress WpdbUnsafeMethodsIssue
    $rows = $wpdb->get_results(
        "SELECT id, datetime, timestamp_gmt, user_login, page, page_time, event, auth_ip, role, user_agent, browser_sign
		FROM " . SPBC_TBL_SECURITY_LOG
        . $wpms_snippet
        . " sent <> 1"
        . " ORDER BY datetime DESC"
        . " LIMIT " . SPBC_SELECT_LIMIT . ";"
    );

    $rows_count = count($rows);

    if ( $rows_count ) {
        $data = array();

        foreach ( $rows as $record ) {
            $page_time = (string) $record->page_time;
            if ((int)$page_time <= 0) {
                $page_time = '1';
            }

            $user_agent = null;
            $browser_signature = null;
            if ( in_array(strval($record->event), array( 'login', 'login_2fa', 'login_new_device', 'logout', )) ) {
                $user_agent = $record->user_agent;
                $browser_signature = $record->browser_sign;
            }

            $security_logs_row_dto = new SecurityLogsDataRowDTO(array(
                'log_id'            => (string) $record->id,
                'datetime'          => (string) $record->datetime,
                'datetime_gmt'      => $record->timestamp_gmt,
                'user_log'          => (string) $record->user_login,
                'event'             => (string) $record->event,
                'auth_ip'           => strpos($record->auth_ip, ':') === false
                    ? (int) sprintf('%u', ip2long($record->auth_ip))
                    : (string) $record->auth_ip,
                'page_url'          => (string) $record->page,
                'event_runtime'     => $page_time,
                'role'              => (string) $record->role,
                'user_agent'        => (string) $user_agent,
                'browser_signature' => (string) $browser_signature,
            ));
            $data[] = $security_logs_row_dto->getArray();
        }

        $security_logs_method_dto = new SecurityLogsDTO(
            array(
                'auth_key'    => $api_key,
                'method_name' => 'security_logs',
                'timestamp'   => current_time('timestamp'),
                'data'        => json_encode($data),
                'rows'        => count($data),
            )
        );

        $result = SpbcAPI::method__security_logs($security_logs_method_dto);

        if ( empty($result['error']) ) {
            // Clear local table if it's ok.
            if ( $result['rows'] == $rows_count ) {
                $updated_ids = array();
                foreach ($data as $item) {
                    $updated_ids[] = $item['log_id'];
                }

                $placeholders = rtrim(str_repeat('%s,', count($updated_ids)), ',');
                if ( SPBC_WPMS ) {
                    $sql = "UPDATE " . SPBC_TBL_SECURITY_LOG . " SET sent = 1 WHERE id IN ($placeholders)"
                        . ( $spbc->ms__work_mode == 2 ? '' : ' AND blog_id = ' . get_current_blog_id() )
                        . ";";
                } else {
                    $sql = "UPDATE " . SPBC_TBL_SECURITY_LOG . " SET sent = 1 WHERE id IN ($placeholders);";
                }
                $wpdb->query($wpdb->prepare($sql, $updated_ids));

                $result = $rows_count;
            } else {
                $result = array(
                    'error' => sprintf(__('Sent: %d. Confirmed receiving of %d rows.', 'security-malware-firewall'), $rows_count, intval($result['rows']))
                );
            }
        }
    } else {
        $result = array(
            'error' => 'NO_LOGS_TO_SEND'
        );
    }

    global $spbc_cron;
    if ( ! empty($spbc_cron) && empty($result['error']) ) {
        $spbc->data['logs_last_sent']         = current_time('timestamp');
        $spbc->data['last_sent_events_count'] = $result;
    }

    $has_unsent_logs = spbc_has_unsent_security_logs();

    if ($has_unsent_logs) {
        SpbcCron::updateTask('send_logs', 'spbc_send_logs', 60, time() + 60);
    } else {
        SpbcCron::updateTask('send_logs', 'spbc_send_logs', 3600, time() + 3600);
    }

    return $result;
}

/**
 * Checks if there are unsent logs persists in the SPBC_TBL_SECURITY_LOG and the count is larger than SPBC_SELECT_LIMIT.
 * Used in cases if logs were generated faster than sending logs cron handled them.
 * @return bool
 */
function spbc_has_unsent_security_logs()
{
    global $wpdb;

    $wpms_snippet = SPBC_WPMS
        ? (" WHERE blog_id = " . get_current_blog_id() . ' AND ')
        : " WHERE ";

    $rows = $wpdb->get_var(
        "SELECT count(id)
		FROM " . SPBC_TBL_SECURITY_LOG
        . $wpms_snippet
        . " sent <> 1"
        . " ORDER BY datetime DESC;"
    );

    return (int)$rows > SPBC_SELECT_LIMIT;
}

/**
 * @return bool
 * @psalm-suppress RedundantCondition
 */
function spbc_set_api_key()
{
    global $spbc;

    $website        = parse_url(get_option('home'), PHP_URL_HOST) . parse_url(get_option('home'), PHP_URL_PATH);
    $platform       = 'wordpress';
    $user_ip        = IP::get();
    $timezone       = (string)get_option('gmt_offset');
    $language       = Server::get('HTTP_ACCEPT_LANGUAGE');
    $is_wpms        = is_multisite() && defined('SUBDOMAIN_INSTALL') && ! SUBDOMAIN_INSTALL;
    $white_label    = false;
    $hoster_api_key = $spbc->ms__hoster_api_key;

    $result = SpbcAPI::method__get_api_key(
        'security',
        get_network_option(0, 'admin_email'),
        $website,
        $platform,
        $timezone,
        $language,
        $user_ip,
        $is_wpms,
        $white_label,
        $hoster_api_key
    );

    if ( ! empty($result['error']) ) {
        $spbc->data['key_is_ok'] = false;
        $spbc->error_add('get_key', $result);

        return false;
    } else {
        $api_key                    = trim($result['auth_key']);
        $api_key                    = preg_match('/^[a-z\d]*$/', $api_key) ? $api_key : $spbc->settings['spbc_key']; // Check key format a-z\d
        $api_key                    = is_main_site() || $spbc->ms__work_mode != 2 ? $api_key : $spbc->network_settings['spbc_key'];
        $spbc->settings['spbc_key'] = $api_key;
        $spbc->save('settings');

        $spbc->data['user_token']  = ( ! empty($result['user_token']) ? $result['user_token'] : '' );
        $spbc->data['key_is_ok']   = spbc_api_key__is_correct($api_key);
        $spbc->data['key_changed'] = true;
        $spbc->save('data');

        $spbc->error_delete('get_key api_key');

        return true;
    }
}

/**
 * The functions check to check an account
 * Executes only via cron (on the main blog)
 *
 * @param null $spbc_key
 *
 * @return array|bool|bool[]|mixed|string[]
 */
function spbc_access_key_notices($spbc_key = null)
{
    global $spbc;

    $spbc_key = $spbc_key ?: $spbc->settings['spbc_key'];

    if ( empty($spbc_key) ) {
        if ( ! $spbc->is_mainsite && $spbc->ms__work_mode != 2 ) {
            $spbc_key = ! empty($spbc->network_settings['spbc_key']) ? $spbc->network_settings['spbc_key'] : false;
            if ( ! $spbc_key ) {
                return array( 'error' => 'KEY_IS_NOT_OK_ON_MAIN_WPMS_SITE' );
            }
        } else {
            $spbc_key =  ! empty($spbc->settings['spbc_key']) ? $spbc->settings['spbc_key'] : false;
            if ( ! $spbc_key ) {
                return array( 'error' => 'KEY_IS_NOT_OK' );
            }
        }
    }

    try {
        spbc_check_account_status($spbc_key);
    } catch (\Exception $exception) {
        return array( 'error' => $exception->getMessage() );
    }

    return true;
}

function spbc_PHP_logs__collect($last_log_sent)
{
    $logs            = array();
    $start_timestamp = time();


    // Try to get log from wp-content/debug/log if default file is not accessible
    $file = ini_get('error_log');
    $file = file_exists($file) && is_readable($file)
        ? $file
        : WP_CONTENT_DIR . '/debug.log';

    if ( file_exists($file) ) {
        if ( is_readable($file) ) {
            // Return if file is empty
            if ( ! filesize($file) ) {
                return array();
            }

            $fd = @fopen($file, 'rb');

            if ( $fd ) {
                $eol = spbc_PHP_logs__detect_EOL_type($file, false);
                if (is_null($eol) || is_int($eol)) {
                    $eol = "\n";
                }

                for (
                    // Initialization
                    $fsize = filesize($file), $offset = 1024 * 5, $position = $fsize - $offset,
                    $max_log_size = 1024 * 1024 * 1, $max_read_size = 1024 * 1024 * 4,
                    $log_size = 0, $read = 0,
                    $log_count = 0;
                    // Conditions
                    $log_size < $max_log_size && // Max usefull data
                    $offset === 1024 * 5 &&        // End of file
                    $read < $max_read_size &&    // Max read depth
                    $log_count < 3500 &&
                    time() < $start_timestamp + 25;
                    // Iteartion adjustments
                    $position -= $offset
                ) {
                    $offset   = $position < 0 ? $offset + $position : $offset;
                    $position = $position < 0 ? 0 : $position;

                    // Set pointer to $it * $offset from the EOF. Or 0 if it's negative.
                    fseek($fd, $position);

                    // Read $offset bytes
                    $it_logs = fread($fd, $offset);

                    // Clean to first EOL, splitting to array by PHP_EOL.
                    if ( $position != 0 ) {
                        $position_adjustment = strpos($it_logs, $eol);
                        $position            += $position_adjustment + 1;
                        $it_logs             = substr($it_logs, $position_adjustment);
                    }

                    $read    += strlen($it_logs);
                    $it_logs = explode($eol, $it_logs);

                    // Filtering and parsing
                    foreach ( $it_logs as $log_line ) {
                        if ( spbc_PHP_logs__filter($log_line, $last_log_sent) ) {
                            $log_size += strlen($log_line);
                            $log_count++;
                            $parsed_log_line = spbc_PHP_logs__parse_line($log_line);
                            if ( $parsed_log_line ) {
                                $logs[] = $parsed_log_line;
                            }
                        }
                    }
                }

                return $logs;
            } else {
                return array( 'error' => 'COULDNT_OPEN_LOG_FILE' );
            }
        } else {
            return array( 'error' => 'LOG_FILE_IS_UNACCESSIBLE' );
        }
    } else {
        return array( 'error' => 'LOG_FILE_NOT_EXISTS' );
    }
}

function spbc_PHP_logs__filter($line, $php_logs_last_sent)
{
    $line = trim($line);

    if ( ! empty($line) ) {
        preg_match('/^\[(.*?\s\d\d:\d\d:\d\d.*?)]/', $line, $matches);
        if ( isset($matches[1]) && strtotime($matches[1]) >= $php_logs_last_sent ) {
            if ( preg_match('/^\[(.*?)\]\s+PHP\s(Warning|Fatal|Notice|Parse)/', $line) ) {
            } else {
                $line = false;
            }
        } else {
            $line = false;
        }
    } else {
        $line = false;
    }

    return $line;
}

function spbc_PHP_logs__parse_line($line)
{
    if ( preg_match('/^\[(.*?)\]\s((.*?):\s+(.+))$/', $line, $matches) ) {
        return array(
            date('Y-m-d H:i:s', strtotime($matches[1])),
            $matches[2],
        );
    }
}

function spbc_PHP_logs__send()
{
    global $spbc;

    if ( empty($spbc->settings['misc__backend_logs_enable']) || empty($spbc->settings['spbc_key']) ) {
        return true;
    }

    $logs = spbc_PHP_logs__collect($spbc->data['last_php_log_sent']);

    if ( empty($logs['error']) ) {
        if ( ! empty($logs) ) {
            $result = SpbcAPI::method__security_backend_logs($spbc->settings['spbc_key'], $logs);

            if ( empty($result['error']) ) {
                if ( isset($result['total_logs_found']) ) {
                    if ( $result['total_logs_found'] == count($logs) ) {
                        $spbc->data['last_php_log_sent']   = time();
                        $spbc->data['last_php_log_amount'] = $result['total_logs_found'];
                        $spbc->save('data');

                        return true;
                    } else {
                        return array( 'error' => 'LOGS_COUNT_DOES_NOT_MATCH' );
                    }
                } else {
                    return array( 'error' => 'LOGS_COUNT_IS_EMPTY' );
                }
            } else {
                return $result;
            }
        } else {
            return true;
        }
    } else {
        return $logs;
    }
}

function spbc_check_ajax_referer($action = - 1, $query_arg = false, $die = true)
{
    $res = true;
    if ( function_exists('check_ajax_referer') ) {
        /** @psalm-suppress ForbiddenCode */
        $res = check_ajax_referer($action, $query_arg, $die);

        if ( $res && ! current_user_can('manage_options') ) {
            if ( $die ) {
                wp_die('-1', 403);
            }
            $res = false;
        }
    }
    return $res;
}

/**
 * Check connection to the API servers
 *
 * @param array $urls_to_test
 *
 * @return array
 */
function spbc_test_connection($urls_to_test = array())
{

    $out          = array();
    $urls_to_test = $urls_to_test ?: array_keys(\CleantalkSP\SpbctWP\Helpers\IP::$cleantalks_servers);

    foreach ( $urls_to_test as $url ) {
        $start  = microtime(true);
        $result = HTTP::getContentFromURL($url, false);

        $out[ $url ] = array(
            'result'    => ! empty($result['error']) ? $result['error'] : 'OK',
            'exec_time' => microtime(true) - $start,
        );
    }

    return $out;
}

function spbc_sync($direct_call = false)
{

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

    global $spbc;

    //Clearing all errors
    $spbc->error_delete_all('and_save_data');

    // If key provided by super admin
    if ( $spbc->is_mainsite || $spbc->ms__work_mode != 2 ) {
        // Checking account status
        try {
            spbc_check_account_status($spbc->api_key);
        } catch ( Exception $exception ) {
            $error_text = $exception->getMessage() === 'KEY_IS_NOT_VALID'
                ? sprintf(__('Access key is not valid. Key: %s.', 'security-malware-firewall'), $spbc->settings['spbc_key'])
                : $exception->getMessage();
            $spbc->error_add('apikey', $error_text);
        }
    }

    // Sending logs.
    $result = spbc_send_logs($spbc->api_key);
    if ( empty($result['error']) ) {
        $spbc->data['logs_last_sent']         = current_time('timestamp');
        $spbc->data['last_sent_events_count'] = $result;
        $spbc->error_delete('send_logs');
    } else {
        $spbc->error_add('send_logs', $result);
    }

    // If key provided by super admin
    if ( is_main_site() ) {
        // Updating signtaures
        $result = spbc_scanner__signatures_update();
        empty($result['error'])
            ? $spbc->error_delete('scanner_update_signatures', 'save')
            : $spbc->error_add('scanner_update_signatures', $result);
    }

    $out = array(
        'success' => true,
        'reload'  => $spbc->data['key_changed'] || !empty($spbc->errors),
    );

    // Sending FW logs
    $result = spbc_send_firewall_logs($spbc->api_key);
    if ( empty($result['error']) ) {
        $spbc->fw_stats['last_send']       = current_time('timestamp');
        $spbc->fw_stats['last_send_count'] = $result;
        $spbc->error_delete('send_firewall_logs');
    } else {
        $spbc->error_add('send_firewall_logs', $result);
    }

    $spbc->data['key_changed'] = false;
    $spbc->save('data');
    $spbc->save('fw_stats', true, false);

    // Do async actions after all so data can't be overwrite by sync actions

    // Updating FW
    //Reset last call of update_sec_fw
    $spbc->remote_calls['update_security_firewall']['last_call'] = 0;
    $spbc->save('remote_calls', true, false);

    $result = spbc_security_firewall_update__init();

    if ( ! empty($result['error']) ) {
        $spbc->error_add('firewall_update', $result['error']);
    }

    // Get custom message for security firewall
    $result_service_get = spbct_perform_service_get();
    if ( ! empty($result_service_get['error']) ) {
        if ($result_service_get['error_no'] !== 403) {
            $spbc->error_add('service_customize', $result_service_get['error']);
        }
    }

    // Update scan settings exclusions
    $result_update_exclusions = spbc_update_scan_settings_exclusions();

    SpbcCron::updateTask('update_scan_settings_exclusions', 'spbc_update_scan_settings_exclusions', 86400);
    if ( ! empty($result_update_exclusions['error']) ) {
        $spbc->error_add('update_exclusions', $result_update_exclusions['error']);
    }

    // Try to adjust to environment
    $adjust = new AdjustToEnvironmentHandler();
    $adjust->handle();

    // Set cron task calling right now
    SpbcCron::updateTask('check_vulnerabilities', 'spbc_security_check_vulnerabilities', 86400, time());

    if ( $direct_call ) {
        return $out;
    }
    wp_send_json($out);
}

function spbct_perform_service_get()
{
    global $spbc;

    $result_service_get = SpbcAPI::method__service_get(
        $spbc->api_key,
        $spbc->data['user_token']
    );

    if ( empty($result_service_get['error']) ) {
        $spbc->settings['fw__custom_message']          = isset($result_service_get['server_response'])
            ? $result_service_get['server_response']
            : '';
        $spbc->save('settings');
    }

    return $result_service_get;
}

// The functions sends daily reports about attempts to login.
function spbc_send_daily_report($skip_data_rotation = false)
{

    if ( ! function_exists('wp_mail') ) {
        add_action('plugins_loaded', 'spbc_send_daily_report');

        return;
    }

    global $spbc, $wpdb, $spbc_tpl;

    //If key is not ok, send daily report!
    if ( ! $spbc->key_is_ok ) {
        include_once SPBC_PLUGIN_DIR . 'templates/spbc_send_daily_report.php';

        // Hours
        $report_interval = 24 * 7;

        $admin_email = spbc_get_admin_email();
        if ( ! $admin_email ) {
            error_log(
                sprintf(
                    '%s: can\'t send the Daily report because of empty Admin email. File: %s, line %d.',
                    $spbc->data["wl_brandname"],
                    __FILE__,
                    __LINE__
                )
            );

            return false;
        }

        $sql  = sprintf(
            'SELECT id,datetime,user_login,event,auth_ip,page,page_time
			FROM %s WHERE datetime between now() - interval %d hour and now();',
            SPBC_TBL_SECURITY_LOG,
            $report_interval
        );
        $rows = $wpdb->get_results($sql);
        foreach ( $rows as $k => $v ) {
            if ( isset($v->datetime) ) {
                $v->datetime_ts = strtotime($v->datetime);
            }
            $rows[$k] = $v;
        }
        usort($rows, "spbc_usort_desc");

        $record_datetime         = time();
        $events                  = array();
        $auth_failed_events      = array();
        $invalid_username_events = array();
        $auth_failed_count       = 0;
        $invalid_username_count  = 0;
        $ips_data                = '';
        foreach ( $rows as $record ) {
            if ( strtotime($record->datetime) > $record_datetime ) {
                $record_datetime = strtotime($record->datetime);
            }
            $events[ $record->event ][ $record->user_login ][] = array(
                'datetime'   => $record->datetime,
                'auth_ip'    => $record->auth_ip,
                'user_login' => $record->user_login,
                'page'       => $record->page ?: '-',
                'page_time'  => $record->page_time ?: 'Unknown'
            );

            switch ( $record->event ) {
                case 'auth_failed':
                    $auth_failed_events[ $record->user_login ][ $record->auth_ip ] = array(
                        'attempts'   => isset($auth_failed_events[ $record->user_login ][ $record->auth_ip ]['attempts'])
                            ? $auth_failed_events[ $record->user_login ][ $record->auth_ip ]['attempts'] + 1
                            : 1,
                        'auth_ip'    => $record->auth_ip,
                        'user_login' => $record->user_login
                    );
                    $auth_failed_count++;
                    break;
                case 'invalid_username':
                    $invalid_username_events[ $record->user_login ][ $record->auth_ip ] = array(
                        'attempts'   => isset($invalid_username_events[ $record->user_login ][ $record->auth_ip ]['attempts'])
                            ? $invalid_username_events[ $record->user_login ][ $record->auth_ip ]['attempts'] + 1
                            : 1,
                        'auth_ip'    => $record->auth_ip,
                        'user_login' => $record->user_login
                    );
                    $invalid_username_count++;
                    break;
            }
            if ( $ips_data != '' ) {
                $ips_data .= ',';
            }
            $ips_data .= $record->auth_ip;
        }

        $ips_c = spbc_get_countries_by_ips($ips_data);

        $event_part       = '';
        $auth_failed_part = sprintf(
            "<p style=\"color: #666;\">%s</p>",
            _("0 brute force attacks have been made for past day.")
        );
        if ( $auth_failed_count ) {
            foreach ( $auth_failed_events as $e ) {
                $ip_part = '';
                foreach ( $e as $ip ) {
                    $country_part = spbc_report_country_part($ips_c, $ip['auth_ip']);
                    $ip_part      .= sprintf(
                        "<a href=\"https://cleantalk.org/blacklists/%s\">%s</a>, #%d, %s<br />",
                        $ip['auth_ip'],
                        $ip['auth_ip'],
                        $ip['attempts'],
                        $country_part
                    );
                }
                $event_part .= sprintf($spbc_tpl['event_part_tpl'], $ip['user_login'], $ip_part);
            }
            $auth_failed_part = sprintf($spbc_tpl['auth_failed_part'], $event_part);
        }

        $invalid_username_part = sprintf(
            "<p style=\"color: #666;\">%s</p>",
            _('0 brute force attacks have been made for past day.')
        );

        if ( $invalid_username_count ) {
            foreach ( $invalid_username_events as $e ) {
                $ip_part = '';
                foreach ( $e as $ip ) {
                    $country_part = spbc_report_country_part($ips_c, $ip['auth_ip']);
                    $ip_part      .= sprintf(
                        "<a href=\"https://cleantalk.org/blacklists/%s\">%s</a>, #%d, %s<br />",
                        $ip['auth_ip'],
                        $ip['auth_ip'],
                        $ip['attempts'],
                        $country_part
                    );
                }
                $event_part .= sprintf(
                    $spbc_tpl['event_part_tpl'],
                    $ip['user_login'],
                    $ip_part
                );
            }
            $invalid_username_part = sprintf($spbc_tpl['auth_failed_part'], $event_part);
        }

        $logins_part = sprintf(
            "<p style=\"color: #666;\">%s</p>",
            _('0 users have been logged in for past day.')
        );
        if ( isset($events['login']) && count($events['login']) ) {
            $event_part = '';
            foreach ( $events['login'] as $user_login => $e ) {
                $l_part = '';
                foreach ( $e as $e2 ) {
                    $country_part = spbc_report_country_part($ips_c, $e2['auth_ip']);
                    $l_part       .= sprintf(
                        "%s, <a href=\"https://cleantalk.org/blacklists/%s\">%s</a>, %s<br />",
                        date("M d Y H:i:s", strtotime($e2['datetime'])),
                        $e2['auth_ip'],
                        $e2['auth_ip'],
                        $country_part
                    );
                }
                $event_part .= sprintf(
                    $spbc_tpl['event_part_tpl'],
                    $user_login,
                    $l_part
                );
            }
            $logins_part = sprintf(
                $spbc_tpl['logins_part_tpl'],
                $event_part
            );
        }

        $title_main_part = _('Daily security report');
        $subject         = sprintf(
            '%s %s',
            parse_url(get_option('home'), PHP_URL_HOST),
            $title_main_part
        );

        $message_anounce = sprintf(
            _('%s brute force attacks or failed logins, %d successful logins.'),
            number_format($auth_failed_count + $invalid_username_count, 0, ',', ' '),
            isset($events['login']) ? count($events['login']) : 0
        );


        $message = sprintf(
            $spbc_tpl['message_tpl'],
            $spbc_tpl['message_style'],
            $title_main_part,
            $message_anounce,
            $auth_failed_part,
            $invalid_username_part,
            $logins_part,
            $spbc->data["wl_brandname"]
        );


        $headers = array('Content-Type: text/html; charset=UTF-8');
        wp_mail(
            $admin_email,
            $subject,
            $message,
            $headers
        );

        if ( ! $skip_data_rotation ) {
            $sql = sprintf(
                "delete from %s where datetime <= '%s';",
                SPBC_TBL_SECURITY_LOG,
                date("Y-m-d H:i:s", $record_datetime)
            );
            $wpdb->query($sql);
        };
    }

    return null;
}

function spbc_private_list_add()
{
    global $spbc, $current_user;

    $ip = IP::get();

    if ( Cookie::get('spbc_secfw_ip_wl') === md5($ip . $spbc->spbc_key) ) {
        return;
    }

    if ( in_array('administrator', $current_user->roles) ) {
        $res = spbc_private_list_add_api_call($ip);
        if ( $res ) {
            if ( ! headers_sent() ) {
                $cookie_val = md5($ip . $spbc->spbc_key);
                Cookie::set('spbc_secfw_ip_wl', $cookie_val, time() + 86400 * 25, '/', '', false, true);
            }

            // Add to the local database
            $status_for_db = 1;
            $version = IP::validate($ip);
            if ( $version === 'v4' ) {
                $data[] = ip2long($ip) . ',' . ip2long('255.255.255.255') . ',' . $status_for_db;
            } elseif ( $version === 'v6' ) {
                $data[] = $ip . ',' . '128' . ',' . $status_for_db;
            } else {
                wp_send_json_error('Local database: adding IP ' . $ip . ' failed: ip does not look like a valid IP address');
            }
            try {
                $res_local = spbct_sfw_private_records_handler('add', json_encode($data, JSON_FORCE_OBJECT));
                wp_send_json_success($res_local);
            } catch (\Exception $e) {
                wp_send_json_error('Local database: adding IP ' . $ip . ' failed: ' . $e->getMessage());
            }
        }
        wp_send_json_error('API wrong answer.');
    }
}

function spbc_private_list_add_api_call($ip)
{
    global $spbc;
    if ( IP::validate($ip) !== false ) {
        $res = SpbcAPI::method__private_list_add__secfw_wl($spbc->user_token, $ip, $spbc->data['service_id']);

        return isset($res['records'][0]['operation_status']) && $res['records'][0]['operation_status'] === 'SUCCESS';
    }

    return false;
}

/**
 * Cron. Update statuses of files sent to the cloud sandbox.
 */
function spbc_scanner_update_pscan_files_status()
{
    global $wpdb;
    // Reading DB for NEW files
    $undone_files_list = $wpdb->get_results(
        'SELECT fast_hash'
        . ' FROM ' . SPBC_TBL_SCAN_FILES
        . ' WHERE pscan_processing_status <> "DONE" AND pscan_processing_status IS NOT NULL',
        ARRAY_A
    );

    if ( !empty($undone_files_list) && is_admin()) {
        $files_fast_hashes_to_update = array();
        foreach ( $undone_files_list as $file ) {
            $files_fast_hashes_to_update[] = $file['fast_hash'];
        }
        spbc_scanner_pscan_check_analysis_status(true, $files_fast_hashes_to_update);
    } else {
        \CleantalkSP\SpbctWP\Cron::removeTask('scanner_update_pscan_files_status');
    }
}

/**
 * Cron. Resend files that were not added to the cloud sandbox queue.
 */
function spbc_scanner_resend_pscan_files($do_rescan = true)
{
    $pending_queue_files = ScanRepository::getPendingQueueFiles();

    if (!empty($pending_queue_files)) {
        foreach ($pending_queue_files as $file) {
            // fix for files sent to manual analysis
            if (!empty($file['status']) && $file['status'] === 'APPROVED_BY_CT') {
                ScanStorage::setFileAsNotPendingQueue($file['fast_hash']);
                continue;
            }

            spbc_scanner_file_send(true, $file['fast_hash'], $do_rescan);
        }
    } else {
        \CleantalkSP\SpbctWP\Cron::removeTask('scanner_resend_pscan_files');
    }
}

function spbc_check_account_status($api_key)
{
    global $spbc, $plugin_info;

    // Checking account status
    $result = SpbcAPI::method__notice_paid_till(
        $api_key,
        preg_replace('/http[s]?:\/\//', '', get_option('home'), 1), // Site URL
        'security'
    );

    // API returns an error
    if ( ! empty($result['error']) ) {
        $spbc->data['key_is_ok'] = false;
        $spbc->save('data');
        throw new \RuntimeException($result['error']);
    }

    // Key is not valid
    if ( ! $result['valid'] ) {
        $spbc->data['key_is_ok'] = false;
        $spbc->data['notice_trial'] = 0;
        $spbc->save('data');
        throw new \RuntimeException('KEY_IS_NOT_VALID');
    }

    $spbc->data['key_is_ok'] = true;

    if ( isset($result['user_token']) ) {
        $spbc->data['user_token'] = $result['user_token'];
    }
    $spbc->data['notice_show']                   = isset($result['show_notice']) ? $result['show_notice'] : 0;
    $spbc->data['notice_renew']                  = isset($result['renew']) ? $result['renew'] : 0;
    $spbc->data['notice_trial']                  = isset($result['trial']) ? $result['trial'] : 0;
    $spbc->data['notice_review']                 = isset($result['show_review']) ? (int)$result['show_review'] : 0;
    $spbc->data['service_id']                    = isset($result['service_id']) ? $result['service_id'] : 0;
    $spbc->data['user_id']                       = isset($result['user_id']) ? $result['user_id'] : 0;
    $spbc->data['moderate']                      = isset($result['moderate']) ? $result['moderate'] : 0;
    $spbc->data['license_trial']                 = isset($result['license_trial']) ? $result['license_trial'] : 0;
    $spbc->data['account_name_ob']               = isset($result['account_name_ob']) ? $result['account_name_ob'] : '';
    $spbc->data['extra_package']['backend_logs'] = isset($result['extra_package']) && is_array($result['extra_package']) && in_array('backend_logs', $result['extra_package'], true)
        ? 1
        : 0;

    //todo:temporary solution for description, until we found the way to transfer this from cloud
    if (defined('SPBC_WHITELABEL_PLUGIN_DESCRIPTION')) {
        $result['wl_plugin_description'] = SPBC_WHITELABEL_PLUGIN_DESCRIPTION;
    }

    //todo:temporary solution for FAQ
    if (defined('SPBC_WHITELABEL_FAQ_LINK')) {
        $result['wl_faq_url'] = SPBC_WHITELABEL_FAQ_LINK;
    }

    if ( $spbc->is_network && $spbc->is_mainsite && $spbc->ms__work_mode == 1 ) {
        $spbc->data['services_count ']      = isset($result['services_count']) ? $result['services_count'] : '';
        $spbc->data['services_max']         = isset($result['services_max']) ? $result['services_max'] : '';
        $spbc->data['services_utilization'] = isset($result['services_utilization']) ? $result['services_utilization'] : '';
    }

    if ( isset($result['wl_status']) && $result['wl_status'] === 'ON' ) {
        $spbc->data['wl_mode_enabled'] = true;
        $spbc->data['wl_brandname']     = isset($result['wl_brandname'])
            ? Sanitize::cleanTextField($result['wl_brandname'])
            : $spbc->default_data['wl_brandname'];
        $spbc->data['wl_url']           = isset($result['wl_url'])
            ? Sanitize::cleanUrl($result['wl_url'])
            : $spbc->default_data['wl_url'];

        if (isset($result['wl_faq_url'])) {
            $spbc->data['wl_support_faq'] = Sanitize::cleanUrl($result['wl_faq_url']);
        } elseif (isset($result['wl_support_url'])) {
            $spbc->data['wl_support_faq'] = Sanitize::cleanUrl($result['wl_support_url']);
        } else {
            $spbc->data['wl_support_faq'] = $spbc->default_data['wl_support_url'];
        }

        $spbc->data['wl_support_url']   = isset($result['wl_support_url'])
            ? Sanitize::cleanUrl($result['wl_support_url'])
            : $spbc->default_data['wl_support_url'];
        $spbc->data['wl_support_email'] = isset($result['wl_support_email'])
            ? Sanitize::cleanEmail($result['wl_support_email'])
            : $spbc->default_data['wl_support_email'];
        $spbc->data['wl_plugin_description']     = isset($result['wl_plugin_description'])
            ? Sanitize::cleanTextField($result['wl_plugin_description'])
            : $plugin_info['Description'];
    } else {
        $spbc->data['wl_mode_enabled'] = false;
        $spbc->data['wl_brandname']     = $spbc->default_data['wl_brandname'];
        $spbc->data['wl_url']           = $spbc->default_data['wl_url'];
        $spbc->data['wl_support_faq']   = $spbc->default_data['wl_support_url'];
        $spbc->data['wl_support_url']   = $spbc->default_data['wl_support_url'];
        $spbc->data['wl_support_email'] = $spbc->default_data['wl_support_email'];
    }

    // Disable/enable the collecting backend PHP log depends on the extra package data
    $spbc->settings['misc__backend_logs_enable'] = (int)(
        $spbc->data['extra_package']['backend_logs'] == 1 &&
        $spbc->settings['misc__backend_logs_enable'] == 1
    );
    $spbc->save('settings');
    $spbc->save('data');

    if ( SPBC_WPMS ) {
        $spbc->network_settings['moderate']  = $spbc->data['moderate'];
        $spbc->network_settings['key_is_ok'] = $spbc->data['key_is_ok'];
        $spbc->save('network_settings');
    }
}

/**
 * Clears the table with security logs. Leaves only 50 entries.
 */
function spbc_security_log_clear()
{
    global $spbc, $wpdb;

    $remain_ids = array();

    // Getting ids of last 50 rows
    try {
        $ids = $wpdb->get_results(
            "SELECT id
		    FROM " . SPBC_TBL_SECURITY_LOG
            . " WHERE sent=1"
            . ( SPBC_WPMS ? " AND blog_id = " . get_current_blog_id() : '' )
            . " ORDER BY datetime DESC"
            . " LIMIT 50;",
            'ARRAY_N'
        );

        if ($ids) {
            foreach ($ids as $id) {
                $remain_ids[] = $id[0];
            }
        }
    } catch (\Exception $e) {
        return false;
    }

    if (empty($remain_ids)) {
        return false;
    }

    $wpms_query_part = '';
    if ( SPBC_WPMS && $spbc->ms__work_mode == 2 ) {
        $wpms_query_part = ' AND blog_id = ' . get_current_blog_id();
    }

    $placeholders = rtrim(str_repeat('%s,', count($remain_ids)), ',');
    $query = "DELETE FROM " . SPBC_TBL_SECURITY_LOG . " WHERE sent = 1 AND id NOT IN ($placeholders) " . $wpms_query_part . ";";

    $wpdb->query($wpdb->prepare($query, $remain_ids));

    return true;
}

/**
 * Check whether the request is AMP
 *
 * @return bool
 */
function spbc_is_amp_request()
{
    if (function_exists('amp_is_request')) {
        return amp_is_request();
    }

    return false;
}

/**
 * Parse CDN checker self-request to find CDN headers.
 * @return array|null[]|string[]
 */
function spbc_cdn_checker__parse_request()
{
    global $spbc;
    if ($spbc->settings['secfw__get_ip__enable_cdn_auto_self_check']) {
        return CDNHeadersChecker::check();
    }
    return array('error' => 'CDN checker disabled');
}

/**
 * Send test request to host. Then it should be parsed to find CDN headers.
 * @return void
 * @psalm-suppress
 */
function spbc_cdn_checker__send_request()
{
    global $spbc;
    if ($spbc->settings['secfw__get_ip__enable_cdn_auto_self_check']) {
        CDNHeadersChecker::sendCDNCheckerRequest();
    }
}

/**
 * Current site admin e-mail
 * @return string Admin e-mail
 */
function spbc_get_admin_email()
{
    global $spbc;

    if ( ! is_multisite() ) {
        $admin_email = get_option('admin_email');
    } else {
        $admin_email = get_blog_option(get_current_blog_id(), 'admin_email');
    }

    if ( $spbc->data['account_email'] ) {
        add_filter('spbc_get_api_key_email', function () {
            global $spbc;
            return $spbc->data['account_email'];
        });
    }

    return $admin_email;
}
