<?php

namespace CleantalkSP\SpbctWP\Firewall;

use CleantalkSP\Security\Firewall\Result;
use CleantalkSP\SpbctWP\Helpers\Data;
use CleantalkSP\SpbctWP\Helpers\IP;
use CleantalkSP\SpbctWP\VulnerabilityAlarm\VulnerabilityAlarm;
use CleantalkSP\Variables\Server;
use CleantalkSP\SpbctWP\Escape;

/**
 * @psalm-suppress UnusedClass
 */
class UploadCheckWPModules
{
    public static $module_name = 'UploadCheckWPModules';

    /**
     * Filter for WordPress hook 'upgrader_source_selection'. Do filter uploaded module.
     * @param $source
     * @param $remote_source
     * @param \WP_Upgrader $upgrader
     * @param $args_hook_extra
     * @return \WP_Error
     * @psalm-suppress PossiblyUnusedMethod
     */
    public static function runCheckForWordpressModules(
        $source,
        $remote_source,
        \WP_Upgrader $upgrader,
        $args_hook_extra
    ) {
        global $spbc;

        // Show initial check message
        show_message($spbc->data["wl_brandname"] . sprintf(' Signatures analysis is checking the uploaded %s&#8230;', $args_hook_extra['type']));

        // Prepare and run scan
        $dir_scan = new \CleantalkSP\SpbctWP\Scanner\DirectoryScan(
            $source,
            \CleantalkSP\SpbctWP\Scanner\Controller::getRootPath(),
            array(
                'output_file_details' => array('path', 'full_hash'),
            )
        );

        // Output the result
        $details = '<div id="spbct-upload-checker-details">';
        $details .= '<ul>';

        $overall_result = true;
        $total_files_checked_count = 0;

        try {
            $dir_scan->setElements();
            $results = $dir_scan->scan(true);
            $total_files_checked_count = count($results);
            foreach ($results as $result) {
                // if http error collected or result array invalid
                if ( !empty($result['error']) || in_array(array('path','status','severity'), array_keys($result))) {
                    $details .= '<li>&nbsp;&nbsp;<b>'
                    . __('Error occurred while checking file', 'security_malware_firewall')
                    . '</b>'
                    . ' '
                    . (!empty($result['path']) ? $result['path'] : '')
                    . ':'
                    . ' '
                    . !empty($result['error']) ? $result['error'] : __('internal directory scan error', 'security_malware_firewall')
                        . "</li>";
                } else {
                    $severity = $result['severity'];
                    $file_is_bad = !empty($severity) && is_string($severity) && in_array($severity, array('CRITICAL', 'SUSPICIOUS', 'DANGER'));
                    if ($file_is_bad) {
                        // Cutting useless path prefix
                        $title = json_encode($result['weak_spots']);
                        $title = $title ? esc_html($title) : 'Unknown weak spots';
                        $display_path = preg_replace('#^.wp-content.upgrade[\\\\].+?[\\\\]#', '', $result['path']);
                        $details .= "<li><a title='Weak spots JSON: $title'>&nbsp;&nbsp;&nbsp;&nbsp;$display_path: <b>{$result['status']}</b></a></li>";
                        $overall_result = false;
                        $fired_result = $result;
                    }
                }
            }
        } catch (\Exception $e) {
            $details = '<li>&nbsp;&nbsp;<b>'
                . __('internal directory scan error', 'security_malware_firewall')
                . ':'
                . ' '
                . $e;
            $overall_result = true;
        }

        $details .= '</ul>';
        $details .= '</div>';

        show_message('&nbsp;&nbsp;' . __('Checked files count: ', 'security-malware-firewall') . $total_files_checked_count);

        // Output result message
        if ( $overall_result ) {
            if (self::checkVulnerability($source)) {
                $message = '<b>' . __('If you want to continue the installation, add the file and click install.', 'security-malware-firewall') . '</b>';
                echo Escape::escKsesPreset($message, 'spbc_cdn_checker_table');
                ?>
                <form method="post" enctype="multipart/form-data" class="wp-upload-form"
                action="<?php echo esc_url(self_admin_url('update.php?action=upload-plugin')); ?>">
                    <?php wp_nonce_field('plugin-upload'); ?>
                    <label class="screen-reader-text" for="pluginzip">
                        <?php
                        /* translators: Hidden accessibility text. */
                        _e('Plugin zip file', 'security-malware-firewall');
                        ?>
                    </label>
                    <input type="hidden" name="trust_module" value="true">
                    <input type="hidden" name="file_path" id="file_path">
                    <input type="file" id="pluginzip" name="pluginzip" accept=".zip"/>
                    <?php submit_button(__('Install Now', 'security-malware-firewall'), '', 'install-plugin-submit', false); ?>
                </form>
                <?php

                return new \WP_Error(
                    'spbct.plugin_check.malware_found',
                    '<b>' . __('Found vulnerabilities ', 'security-malware-firewall') . '. Installation interrupted.</b>'
                );
            }
            show_message('&nbsp;&nbsp;<b>No malware has been found. Installation continues.</b>');
        } else {
            show_message('&nbsp;&nbsp;' . __('List of infected files:', 'security-malware-firewall'));
            show_message($details);
            // Remove the directory with bad plugin
            Data::removeDirectoryRecursively($source);

            $reason = __('Malicious signature found.', 'security-malware-firewall');
            if ( isset($fired_result) && !empty($fired_result['weak_spots']) && !empty($fired_result['weak_spots']['SIGNATURES']) ) {
                $signatures = $fired_result['weak_spots']['SIGNATURES'];
                foreach ($signatures as $_row => $signature_ids) {
                    $reason_signature_ids[] = is_array($signature_ids) ? implode(array_values($signature_ids)) : $signature_ids;
                }
                $reason = __('Malicious signatures found: ', 'security-malware-firewall') . '#' . implode(',#', $reason_signature_ids);
            }
            self::logDenyUploadedModule(new Result(
                array(
                    'module' => self::$module_name,
                    'ip' => IP::get(),
                    'status' => 'DENY_BY_WAF_FILE',
                    'pattern' => $reason,
                    'triggered_for' => 'uploaded_module',
                    'waf_action' => 'DENY',
                )
            ));

            return new \WP_Error(
                'spbct.plugin_check.malware_found',
                '<b>Malware has been found. Installation interrupted.</b>'
            );
        }

        return $source;
    }

    /**
     * Log a record. The same code as WAF log. :(
     * @param Result $upload_checker_result
     * @return void
     */
    private static function logDenyUploadedModule(Result $upload_checker_result)
    {
        global $wpdb;

        //single quote escaping
        foreach ($upload_checker_result->pattern as &$pattern ) {
            $pattern = str_replace(array("'", '"'), array("ESC_S_QUOTE", "ESC_D_QUOTE"), $pattern);
        }
        unset($pattern);

        // Truncate before escaping — addslashes-then-substr left a dangling \ at the cut (SQLi).
        $pattern = ! empty($upload_checker_result->pattern)
            ? json_encode($upload_checker_result->pattern)
            : '';
        $triggered_for = ! empty($upload_checker_result->triggered_for)
            ? substr($upload_checker_result->triggered_for, 0, 100)
            : '';
        $page_url = substr(
            (Server::getString('HTTPS') !== 'off' ? 'https://' : 'http://')
            . Server::getString('HTTP_HOST')
            . Server::get('REQUEST_URI', null, 'url'),
            0,
            4096
        );
        $http_user_agent = Server::getString('HTTP_USER_AGENT')
            ? htmlspecialchars(substr(Server::getString('HTTP_USER_AGENT'), 0, 300))
            : 'unknown';
        $ip              = (string) $upload_checker_result->ip;
        $time            = time();
        $status          = (string) $upload_checker_result->status;
        $request_method  = substr(Server::getString('REQUEST_METHOD'), 0, 5);
        $x_forwarded_for = htmlspecialchars(substr(Server::getString('HTTP_X_FORWARDED_FOR'), 0, 15));
        $network         = ($upload_checker_result->network === '' || $upload_checker_result->network === null)
            ? ''
            : (string) $upload_checker_result->network;
        $mask            = ($upload_checker_result->mask === '' || $upload_checker_result->mask === null)
            ? ''
            : (string) $upload_checker_result->mask;
        $is_personal     = (int) $upload_checker_result->is_personal;
        $country_code    = (string) $upload_checker_result->country_code;
        $id              = md5(
            $upload_checker_result->ip
            . $http_user_agent
            . $upload_checker_result->status
            . $upload_checker_result->waf_action
            . $upload_checker_result->triggered_for
        );
        $signature_id    = (int) $upload_checker_result->signature_id;

        $query = "INSERT INTO " . SPBC_TBL_FIREWALL_LOG
            . " SET
                entry_id        = %s,
				ip_entry        = %s,
				entry_timestamp = %d,
				status          = %s,
				pattern         = NULLIF(%s, ''),
				signature_id    = NULLIF(%d, 0),
				triggered_for   = NULLIF(%s, ''),
				requests        = 1,
				page_url        = %s,
				http_user_agent = %s,
				request_method  = %s,
				x_forwarded_for = NULLIF(%s, ''),
				network         = NULLIF(%s, ''),
				mask            = NULLIF(%s, ''),
				country_code    = NULLIF(%s, ''),
				is_personal     = %d
			ON DUPLICATE KEY UPDATE
				ip_entry        = ip_entry,
				entry_timestamp = %d,
				status          = %s,
				pattern         = NULLIF(%s, ''),
				signature_id    = NULLIF(%d, 0),
				triggered_for   = NULLIF(%s, ''),
				requests        = requests + 1,
				page_url        = %s,
				http_user_agent = http_user_agent,
				request_method  = %s,
				x_forwarded_for = NULLIF(%s, ''),
				network         = NULLIF(%s, ''),
				mask            = NULLIF(%s, ''),
				country_code    = NULLIF(%s, ''),
				is_personal     = %d";

        $wpdb->query(
            $wpdb->prepare(
                $query,
                array(
                    $id,
                    $ip,
                    $time,
                    $status,
                    $pattern,
                    $signature_id,
                    $triggered_for,
                    $page_url,
                    $http_user_agent,
                    $request_method,
                    $x_forwarded_for,
                    $network,
                    $mask,
                    $country_code,
                    $is_personal,
                    $time,
                    $status,
                    $pattern,
                    $signature_id,
                    $triggered_for,
                    $page_url,
                    $request_method,
                    $x_forwarded_for,
                    $network,
                    $mask,
                    $country_code,
                    $is_personal,
                )
            )
        );
    }

    /**
     * @return bool
     */
    public static function checkVulnerability($source)
    {
        $plugin_data_to_check = static::getPluginDataFromSource($source);

        if ( $plugin_data_to_check === false ) {
            $plugin_data_to_check = static::getPluginDataFromFilesGlob($source);
        }

        if ( $plugin_data_to_check === false ) {
            return false;
        }

        return VulnerabilityAlarm::checkSinglePluginViaAPI($plugin_data_to_check['Name'], $plugin_data_to_check['Version']);
    }

    /**
     * Check the glob result of source path to find any php file to get the plugin data.
     * @param string $source Path to the plugin source folder
     * @return array|false
     */
    private static function getPluginDataFromSource($source)
    {
        if ( !empty($source) && is_dir($source) && is_readable($source) ) {
            foreach ( glob($source . '/*.php') as $module_file_path ) {
                if ( !is_file($module_file_path) || !is_readable($module_file_path) ) {
                    continue;
                }
                $plugin_data = @get_file_data($module_file_path, array(
                    'Version' => 'Version',
                    'Name' => 'Plugin Name',
                ));
                if ( empty($plugin_data['Name']) || empty($plugin_data['Version']) ) {
                    continue;
                }
                return array(
                    'Name' => $plugin_data['Name'],
                    'Version' => $plugin_data['Version'],
                );
            }
        }
        return false;
    }

    /**
     * Check the file from $_FILES to get the plugin data.
     * @param string $source Path to the plugin source folder
     * @return array|false
     */
    private static function getPluginDataFromFilesGlob($source)
    {
        if ( !isset($_FILES['pluginzip']['name']) ) {
            return false;
        }

        preg_match('#^([A-Za-z\d]+)#', $_FILES['pluginzip']['name'], $match);
        if ( !isset($match[0]) ) {
            return false;
        }

        $plugin_data = get_plugin_data($source . $match[0] . '.php');

        if ( !empty($plugin_data['Name']) && !empty($plugin_data['Version']) ) {
            return array(
                'Name' => $plugin_data['Name'],
                'Version' => $plugin_data['Version'],
            );
        }

        return false;
    }
}
