<?php
/**
 * Hardening toggles: each setting is a one-line toggle in the UI.
 *
 * @package Brainwerk Security Suite
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Shieldforge_Hardening {

    public static function register( Shieldforge_Loader $loader ) {
        if ( Shieldforge_Settings::get( 'hardening_disable_xmlrpc', true ) ) {
            $loader->add_filter( 'xmlrpc_enabled', __CLASS__, 'return_false' );
        }
        // Even when XML-RPC stays enabled (Jetpack / mobile app), always strip
        // system.multicall. It batches hundreds of credential guesses into one
        // HTTP request, letting an attacker amplify a brute-force while
        // generating far fewer per-request rate-limit data points. No legitimate
        // single-action client needs it; callers can re-add it via this filter.
        $loader->add_filter( 'xmlrpc_methods', __CLASS__, 'strip_xmlrpc_multicall' );
        if ( Shieldforge_Settings::get( 'hardening_disable_editor', true ) && ! defined( 'DISALLOW_FILE_EDIT' ) ) {
            // Define before admin loads.
            define( 'DISALLOW_FILE_EDIT', true );
        }
        if ( Shieldforge_Settings::get( 'hardening_hide_version', true ) ) {
            $loader->add_filter( 'the_generator', __CLASS__, 'return_empty_string' );
            remove_action( 'wp_head', 'wp_generator' );
        }
        if ( Shieldforge_Settings::get( 'hardening_block_authors', true ) ) {
            $loader->add_action( 'template_redirect', __CLASS__, 'block_author_enum' );
            $loader->add_filter( 'rest_endpoints', __CLASS__, 'remove_users_endpoint' );
        }
        if ( Shieldforge_Settings::get( 'hardening_security_headers', true ) ) {
            $loader->add_action( 'send_headers', __CLASS__, 'send_security_headers' );
        }
        if ( Shieldforge_Settings::get( 'hardening_rest_restrict', true ) ) {
            $loader->add_filter( 'rest_authentication_errors', __CLASS__, 'restrict_rest_for_anons' );
        }

        // Always-on small wins.
        $loader->add_action( 'init', __CLASS__, 'block_readme_files' );
        $loader->add_action( 'admin_init', __CLASS__, 'log_plugin_lifecycle_listeners' );
    }

    public static function return_false() { return false; }
    public static function return_empty_string() { return ''; }

    /** Remove the brute-force-amplifying system.multicall XML-RPC method. */
    public static function strip_xmlrpc_multicall( $methods ) {
        if ( is_array( $methods ) ) {
            unset( $methods['system.multicall'] );
        }
        return $methods;
    }

    public static function block_author_enum() {
        if ( is_admin() ) return;
        if ( isset( $_GET['author'] ) && ! is_user_logged_in() ) { // phpcs:ignore WordPress.Security.NonceVerification
            wp_safe_redirect( home_url( '/' ), 301 );
            exit;
        }
    }

    public static function remove_users_endpoint( $endpoints ) {
        if ( is_user_logged_in() ) {
            return $endpoints;
        }
        if ( isset( $endpoints['/wp/v2/users'] ) ) unset( $endpoints['/wp/v2/users'] );
        if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
        return $endpoints;
    }

    public static function send_security_headers() {
        if ( headers_sent() ) {
            return;
        }
        // Per-header gating: each can be turned off independently. Default
        // ON for the well-understood ones; CSP defaults OFF because a wrong
        // policy will visibly break a site (broken images, blank pages).
        if ( Shieldforge_Settings::get( 'hardening_header_xcto', true ) ) {
            self::set_header_if_unset( 'X-Content-Type-Options', 'nosniff' );
        }
        if ( Shieldforge_Settings::get( 'hardening_header_xfo', true ) ) {
            self::set_header_if_unset( 'X-Frame-Options', 'SAMEORIGIN' );
        }
        if ( Shieldforge_Settings::get( 'hardening_header_referrer', true ) ) {
            self::set_header_if_unset( 'Referrer-Policy', 'strict-origin-when-cross-origin' );
        }
        if ( Shieldforge_Settings::get( 'hardening_header_permissions', true ) ) {
            self::set_header_if_unset( 'Permissions-Policy', 'geolocation=(), microphone=(), camera=()' );
        }
        if ( Shieldforge_Settings::get( 'hardening_header_hsts', true ) && is_ssl() ) {
            self::set_header_if_unset( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' );
        }
        if ( Shieldforge_Settings::get( 'hardening_csp_enabled', false ) ) {
            $csp = trim( (string) Shieldforge_Settings::get( 'hardening_csp_value', '' ) );
            if ( '' !== $csp ) {
                $report_only = (bool) Shieldforge_Settings::get( 'hardening_csp_report_only', true );
                $name        = $report_only ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
                self::set_header_if_unset( $name, $csp );
            }
        }
    }

    /**
     * header() but only if the server (or another plugin) hasn't already
     * sent a header by the same name. Avoids accidentally clobbering a
     * stricter policy set at the nginx/Apache level.
     */
    private static function set_header_if_unset( $name, $value ) {
        $needle = strtolower( $name ) . ':';
        foreach ( headers_list() as $h ) {
            if ( 0 === strpos( strtolower( $h ), $needle ) ) {
                return;
            }
        }
        header( $name . ': ' . $value );
    }

    public static function restrict_rest_for_anons( $result ) {
        if ( ! empty( $result ) ) return $result;
        if ( is_user_logged_in() ) return $result;

        $route = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';

        // Endpoints that anonymous visitors should never reach. Each
        // pattern is matched against the request URI.
        $blocked_anon = array(
            '/wp-json/wp/v2/users',     // user enumeration
            '/wp-json/wp/v2/comments',  // bulk-comment scrape (often used to harvest emails)
            '/wp-json/wp/v2/search',    // bulk content discovery
            '/wp-json/wp/v2/settings',  // never anonymous
            '/wp-json/wp/v2/themes',    // theme/version disclosure
            '/wp-json/wp/v2/plugins',   // plugin enumeration
        );

        foreach ( $blocked_anon as $needle ) {
            if ( strpos( $route, $needle ) !== false ) {
                Shieldforge_Logger::log( Shieldforge_Logger::EVT_HARDENING_BLOCK, array(
                    'severity' => Shieldforge_Logger::SEV_WARN,
                    'context'  => array( 'pattern' => 'rest_anon_block', 'route' => $needle ),
                ) );
                return new WP_Error(
                    'shieldforge_rest_denied',
                    __( 'REST API access for anonymous users is restricted.', 'brainwerk-security-suite' ),
                    array( 'status' => 401 )
                );
            }
        }
        return $result;
    }

    public static function block_readme_files() {
        $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
        if ( preg_match( '#/(readme\.html|license\.txt|wp-config\.bak|\.env)$#i', $uri ) ) {
            Shieldforge_Logger::log( Shieldforge_Logger::EVT_HARDENING_BLOCK, array(
                'severity' => Shieldforge_Logger::SEV_WARN,
                'context'  => array( 'pattern' => 'readme_or_sensitive_file' ),
            ) );
            status_header( 404 );
            nocache_headers();
            exit;
        }
    }

    public static function log_plugin_lifecycle_listeners() {
        // Listeners attached lazily so they don't add overhead on every request.
        add_action( 'activated_plugin', array( __CLASS__, 'on_plugin_activated' ), 10, 2 );
        add_action( 'deactivated_plugin', array( __CLASS__, 'on_plugin_deactivated' ), 10, 2 );
        add_action( 'switch_theme', array( __CLASS__, 'on_theme_switched' ), 10, 1 );
        add_action( 'user_register', array( __CLASS__, 'on_user_register' ), 10, 1 );
        add_action( 'set_user_role', array( __CLASS__, 'on_role_changed' ), 10, 3 );
    }

    public static function on_plugin_activated( $plugin, $network_wide ) {
        Shieldforge_Logger::log( Shieldforge_Logger::EVT_PLUGIN_ACTIVATED, array(
            'severity' => Shieldforge_Logger::SEV_WARN,
            'user_id'  => get_current_user_id(),
            'context'  => array( 'plugin' => $plugin, 'network_wide' => (bool) $network_wide ),
        ) );
    }

    public static function on_plugin_deactivated( $plugin, $network_wide ) {
        Shieldforge_Logger::log( Shieldforge_Logger::EVT_PLUGIN_DEACTIV, array(
            'severity' => Shieldforge_Logger::SEV_WARN,
            'user_id'  => get_current_user_id(),
            'context'  => array( 'plugin' => $plugin, 'network_wide' => (bool) $network_wide ),
        ) );
    }

    public static function on_theme_switched( $new_name ) {
        Shieldforge_Logger::log( Shieldforge_Logger::EVT_THEME_SWITCHED, array(
            'severity' => Shieldforge_Logger::SEV_WARN,
            'user_id'  => get_current_user_id(),
            'context'  => array( 'new_theme' => $new_name ),
        ) );
    }

    public static function on_user_register( $user_id ) {
        $user = get_userdata( $user_id );
        Shieldforge_Logger::log( Shieldforge_Logger::EVT_USER_REGISTERED, array(
            'severity' => Shieldforge_Logger::SEV_INFO,
            'user_id'  => $user_id,
            'username' => $user ? $user->user_login : '',
        ) );
    }

    public static function on_role_changed( $user_id, $role, $old_roles ) {
        $sev = ( 'administrator' === $role ) ? Shieldforge_Logger::SEV_CRITICAL : Shieldforge_Logger::SEV_WARN;
        $event = ( 'administrator' === $role ) ? Shieldforge_Logger::EVT_ADMIN_CREATED : Shieldforge_Logger::EVT_ROLE_CHANGED;
        $user = get_userdata( $user_id );
        Shieldforge_Logger::log( $event, array(
            'severity' => $sev,
            'user_id'  => $user_id,
            'username' => $user ? $user->user_login : '',
            'context'  => array( 'new_role' => $role, 'old_roles' => $old_roles ),
        ) );
        if ( 'administrator' === $role ) {
            do_action( 'shieldforge_admin_user_created', $user_id, $role );
        }
    }
}
