<?php
namespace EasyChat\Library;

require_once __DIR__ . '/security/local_native_file_session_handler.php';

class SessionBootstrap {
    const COOKIE_NAME = 'EASYCHATSESSID';
    const OPERATOR_SESSION_LIFETIME_SECONDS = 2592000;

    private static $last_error_code = '';
    private static $owned = false;
    private static $log_dir = '';
    private static $session_path = '';
    private static $session_handler = null;
    private static $previous_session_module = 'files';

    public static function start($log_dir, $emit_cache_headers = true) {
        if (session_status() === PHP_SESSION_ACTIVE) {
            if (self::activeSessionMatches($log_dir)) {
                self::$last_error_code = '';
                self::$owned = true;
                return true;
            }

             
             
             
             
             
            if (!self::detachForeignSession()) {
                self::$owned = false;
                self::$last_error_code = 'foreign_session_active';
                return false;
            }
            $id_cleared = (bool)\EasyChat\Library\SafeRuntime::call(function () {
                session_id('');
                return session_id() === '';
            }, false);
            if (!$id_cleared) {
                self::$last_error_code = 'foreign_session_active';
                return false;
            }
        }

        self::$last_error_code = '';
        self::$owned = false;
        if (headers_sent()) {
            self::$last_error_code = 'headers_sent';
            return false;
        }

        if (!self::configureRuntimeOptions()) {
            self::$last_error_code = 'runtime_options_unsafe';
            return false;
        }
        self::configureCacheLimiter((bool)$emit_cache_headers);
        if (!self::configureCookie()) {
            self::$last_error_code = 'cookie_options_unsafe';
            return false;
        }
        if (!self::configureFileStorage($log_dir)) {
             
             
             
            self::$last_error_code = 'storage_unavailable';
            return false;
        }

        $started = (bool)\EasyChat\Library\SafeRuntime::call(function () {
            return session_start();
        }, false);

        if (!$started || session_status() !== PHP_SESSION_ACTIVE) {
            self::releaseSessionHandler();
            self::$last_error_code = 'start_failed';
            return false;
        }

        if (!self::activeSessionMatches($log_dir)) {
            if (!self::abortUntrustedStartedSession()) {
                self::$last_error_code = 'destroy_failed';
                return false;
            }
            self::$last_error_code = 'start_failed';
            return false;
        }

        self::$owned = true;
        return true;
    }

    public static function isAvailable() {
        return self::$owned
            && session_status() === PHP_SESSION_ACTIVE
            && session_name() === self::COOKIE_NAME
            && self::ownedHandlerMatches(self::$session_path)
            && self::$last_error_code === '';
    }

    public static function lastErrorCode() {
        return self::$last_error_code;
    }

    public static function regenerateId() {
        if (!self::isAvailable()) {
            return false;
        }

        $previous_id = session_id();
        $regenerated = (bool)\EasyChat\Library\SafeRuntime::call(function () {
            return session_regenerate_id(true);
        }, false);

        if (!$regenerated || session_id() === '' || session_id() === $previous_id) {
            self::$last_error_code = 'regenerate_failed';
            return false;
        }

        return true;
    }

    




    public static function verifyPersistence(array $expected = array(), array $absent = array()) {
        if (!self::isAvailable() || self::$log_dir === '') {
            self::$last_error_code = 'persistence_unavailable';
            return false;
        }

        try {
            $probe_key = '__easychat_persistence_' . bin2hex(random_bytes(8));
            $probe_value = bin2hex(random_bytes(16));
        } catch (\Throwable $e) {
            self::$last_error_code = 'persistence_probe_failed';
            return false;
        }

        $session =& \EasyChat\Library\RequestContext::session();
        $session[$probe_key] = $probe_value;
        $session_id = session_id();
        $log_dir = self::$log_dir;

        $closed = (bool)\EasyChat\Library\SafeRuntime::call(function () {
            return session_write_close();
        }, false);
        self::$owned = false;
        if (!$closed || $session_id === '') {
            if (session_status() === PHP_SESSION_ACTIVE && session_name() === self::COOKIE_NAME) {
                self::$owned = true;
                self::destroy();
            }
            if (self::$last_error_code !== 'destroy_failed') {
                self::$last_error_code = 'persistence_write_failed';
            }
            return false;
        }

        $id_selected = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($session_id) {
            session_id($session_id);
            return session_id() === $session_id;
        }, false);
        if (!$id_selected || !self::start($log_dir, false) || session_id() !== $session_id) {
            self::destroy();
            if (self::$last_error_code !== 'destroy_failed') {
                self::$last_error_code = 'persistence_reopen_failed';
            }
            return false;
        }

        $session =& \EasyChat\Library\RequestContext::session();
        $valid = isset($session[$probe_key]) && hash_equals($probe_value, (string)$session[$probe_key]);
        foreach ($expected as $key => $value) {
            if (!array_key_exists($key, $session) || $session[$key] !== $value) {
                $valid = false;
                break;
            }
        }
        if ($valid) {
            foreach ($absent as $key) {
                if (array_key_exists((string)$key, $session)) {
                    $valid = false;
                    break;
                }
            }
        }
        unset($session[$probe_key]);

        if (!$valid) {
            self::destroy();
            if (self::$last_error_code !== 'destroy_failed') {
                self::$last_error_code = 'persistence_readback_failed';
            }
            return false;
        }

        return true;
    }

    public static function destroy() {
        if (!self::$owned || session_status() !== PHP_SESSION_ACTIVE || session_name() !== self::COOKIE_NAME) {
            return false;
        }

        $params = session_get_cookie_params();

        \EasyChat\Library\RequestContext::replaceSession([]);
        $destroyed = (bool)\EasyChat\Library\SafeRuntime::call(function () {
            return session_destroy();
        }, false);
        $inactive = session_status() !== PHP_SESSION_ACTIVE;
        if ($destroyed && $inactive) {
            self::$owned = false;
            self::releaseSessionHandler();
            self::$last_error_code = '';
        } else {
             
             
             
             
             
            self::$owned = session_status() === PHP_SESSION_ACTIVE
                && session_name() === self::COOKIE_NAME;
            self::$last_error_code = 'destroy_failed';
            return false;
        }

        if (!headers_sent()) {
            if (PHP_VERSION_ID >= 70300) {
                $same_site = isset($params['samesite']) && self::sameSiteIsSafe((string)$params['samesite'])
                    ? (string)$params['samesite']
                    : 'Lax';
                setcookie(self::COOKIE_NAME, '', [
                    'expires' => time() - 42000,
                    'path' => isset($params['path']) ? (string)$params['path'] : self::cookiePath(),
                    'domain' => isset($params['domain']) ? (string)$params['domain'] : '',
                    'secure' => (bool)($params['secure'] ?? self::isHttps()),
                    'httponly' => (bool)($params['httponly'] ?? true),
                    'samesite' => $same_site,
                ]);
                return true;
            }
            setcookie(self::COOKIE_NAME, '', time() - 42000, $params['path'], '', (bool)$params['secure'], true);
        }

        return true;
    }

    private static function configureRuntimeOptions() {
        if (headers_sent()) {
            return false;
        }

        ini_set('session.use_cookies', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.use_strict_mode', '1');
        ini_set('session.cookie_httponly', '1');
         
         
         
         
        ini_set('session.gc_maxlifetime', (string)self::OPERATOR_SESSION_LIFETIME_SECONDS);
        ini_set('session.cookie_lifetime', (string)self::OPERATOR_SESSION_LIFETIME_SECONDS);

        return (string)ini_get('session.use_cookies') === '1'
            && (string)ini_get('session.use_only_cookies') === '1'
            && (string)ini_get('session.use_strict_mode') === '1'
            && (string)ini_get('session.cookie_httponly') === '1';
    }

    private static function configureCacheLimiter($emit_cache_headers) {
        if (headers_sent() || !function_exists('session_cache_limiter')) {
            return;
        }

        if (!$emit_cache_headers) {
             
             
             
            session_cache_limiter('');
            return;
        }

        session_cache_limiter('nocache');
    }

    private static function configureCookie() {
        if (headers_sent()) {
            return false;
        }

        if (session_name() !== self::COOKIE_NAME) {
            session_name(self::COOKIE_NAME);
        }

        $path = self::cookiePath();
        $secure = self::isHttps();

        if (PHP_VERSION_ID >= 70300) {
            session_set_cookie_params([
                'lifetime' => self::OPERATOR_SESSION_LIFETIME_SECONDS,
                'path' => $path,
                 
                 
                'domain' => '',
                'secure' => $secure,
                'httponly' => true,
                'samesite' => 'Lax',
            ]);
        } else {
            session_set_cookie_params(self::OPERATOR_SESSION_LIFETIME_SECONDS, $path . '; SameSite=Lax', '', $secure, true);
        }

        $effective = session_get_cookie_params();
        return session_name() === self::COOKIE_NAME
            && self::cookieOptionsAreSafe($effective, $path, $secure);
    }

    private static function configureFileStorage($log_dir) {
        if (headers_sent()) {
            return false;
        }

        $session_path = self::prepareSecureSessionPath($log_dir);
        if ($session_path === '') {
            return false;
        }

        self::$log_dir = rtrim((string)$log_dir, '/\\');
        self::$session_path = $session_path;

         
         
         
        if (self::ownedHandlerMatches($session_path)) {
            return true;
        }

        $previous_module = function_exists('session_module_name') ? (string)session_module_name() : 'files';
        if (function_exists('session_module_name') && $previous_module !== 'files') {
            \EasyChat\Library\SafeRuntime::call(function () {
                return session_module_name('files');
            }, false);
        }
        if (function_exists('session_module_name') && session_module_name() !== 'files') {
            return false;
        }

        $handler = \EasyChat\Library\SafeRuntime::call(function () use ($session_path) {
            return new \EasyChat\Library\Security\LocalNativeFileSessionHandler(
                $session_path,
                self::COOKIE_NAME,
                self::OPERATOR_SESSION_LIFETIME_SECONDS
            );
        }, false);
        if (!$handler instanceof \EasyChat\Library\Security\LocalNativeFileSessionHandler) {
            return false;
        }
        $registered = (bool)\EasyChat\Library\SafeRuntime::call(function () use ($handler) {
            return session_set_save_handler($handler, true);
        }, false);
        if (!$registered || (function_exists('session_module_name') && session_module_name() !== 'user')) {
            if (function_exists('session_module_name')) {
                \EasyChat\Library\SafeRuntime::call(function () {
                    return session_module_name('files');
                }, false);
            }
            return false;
        }

        self::$session_handler = $handler;
        self::$previous_session_module = $previous_module === 'user' ? 'files' : $previous_module;
        return self::ownedHandlerMatches($session_path);
    }

    private static function activeSessionMatches($log_dir) {
        $session_path = self::prepareSecureSessionPath($log_dir);
        if ($session_path === ''
            || session_name() !== self::COOKIE_NAME
            || !self::ownedHandlerMatches($session_path)
            || !self::runtimeOptionsAreSecure()
        ) {
            return false;
        }

        $params = session_get_cookie_params();
        if (!self::cookieOptionsAreSafe($params, self::cookiePath(), self::isHttps())) {
            return false;
        }

        self::$log_dir = rtrim((string)$log_dir, '/\\');
        self::$session_path = $session_path;
        return true;
    }

    private static function ownedHandlerMatches($session_path) {
        return self::$session_handler instanceof \EasyChat\Library\Security\LocalNativeFileSessionHandler
            && (!function_exists('session_module_name') || session_module_name() === 'user')
            && self::$session_handler->isBoundTo((string)$session_path, self::COOKIE_NAME);
    }

    private static function abortUntrustedStartedSession() {
        $destroyed = true;
        if (session_status() === PHP_SESSION_ACTIVE) {
            \EasyChat\Library\RequestContext::replaceSession([]);
            $destroyed = (bool)\EasyChat\Library\SafeRuntime::call(function () {
                return session_destroy();
            }, false);
        }
        if (!$destroyed || session_status() === PHP_SESSION_ACTIVE) {
            self::$owned = session_status() === PHP_SESSION_ACTIVE
                && session_name() === self::COOKIE_NAME;
            return false;
        }
        self::$owned = false;
        self::releaseSessionHandler();
        return true;
    }

    private static function releaseSessionHandler() {
        if (session_status() === PHP_SESSION_ACTIVE || self::$session_handler === null) {
            return;
        }
        $restore = self::$previous_session_module;
        if (!is_string($restore) || $restore === '' || $restore === 'user') {
            $restore = 'files';
        }
        if (function_exists('session_module_name')) {
            \EasyChat\Library\SafeRuntime::call(function () use ($restore) {
                return session_module_name($restore);
            }, false);
            if (session_module_name() === 'user') {
                \EasyChat\Library\SafeRuntime::call(function () {
                    return session_module_name('files');
                }, false);
            }
        }
        self::$session_handler = null;
        self::$previous_session_module = 'files';
        self::$session_path = '';
        self::$log_dir = '';
    }

    private static function prepareSecureSessionPath($log_dir) {
        $log_dir = rtrim((string)$log_dir, '/\\');
        if ($log_dir === '' || strpos($log_dir, "\0") !== false || is_link($log_dir)) {
            return '';
        }

        if (!is_dir($log_dir) && !\EasyChat\Library\SafeRuntime::mkdir($log_dir, 0700, true)) {
            return '';
        }
        $session_path = $log_dir . DIRECTORY_SEPARATOR . 'sessions';
        if (is_link($session_path)) {
            return '';
        }
        if (!is_dir($session_path) && !\EasyChat\Library\SafeRuntime::mkdir($session_path, 0700, true)) {
            return '';
        }
        if (!is_dir($session_path) || is_link($session_path)) {
            return '';
        }

        $real_log = realpath($log_dir);
        $real_session = realpath($session_path);
        if ($real_log === false || $real_session === false) {
            return '';
        }
        $real_log_prefix = rtrim($real_log, '/\\') . DIRECTORY_SEPARATOR;
        if (strpos($real_session . DIRECTORY_SEPARATOR, $real_log_prefix) !== 0) {
            return '';
        }

        $mode = \EasyChat\Library\SafeRuntime::permissions($real_session);
        if ($mode === null) {
            return '';
        }
         
         
         
        $hardened_mode = 0700;
        if ($hardened_mode !== $mode && !\EasyChat\Library\SafeRuntime::chmod($real_session, $hardened_mode)) {
            return '';
        }
        clearstatcache(true, $real_session);
        $effective_mode = \EasyChat\Library\SafeRuntime::permissions($real_session);
        if ($effective_mode === null || ($effective_mode & 0077) !== 0 || !is_writable($real_session)) {
            return '';
        }

        return self::probeSessionStorage($real_session) ? $real_session : '';
    }

    private static function probeSessionStorage($session_path) {
        try {
            $probe = $session_path . DIRECTORY_SEPARATOR . '.easychat-session-probe-' . bin2hex(random_bytes(12));
            $renamed = $probe . '.checked';
            $payload = bin2hex(random_bytes(16));
        } catch (\Throwable $e) {
            return false;
        }

        return (bool)\EasyChat\Library\SafeRuntime::call(function () use ($session_path, $probe, $renamed, $payload) {
            $handle = fopen($probe, 'x+b');
            if ($handle === false) {
                return false;
            }
            $written = fwrite($handle, $payload);
            $flushed = fflush($handle);
            fclose($handle);
            if ($written !== strlen($payload) || !$flushed || !chmod($probe, 0600)) {
                unlink($probe);
                return false;
            }

            clearstatcache(true, $probe);
            $probe_mode = fileperms($probe) & 0777;
            $dir_owner = fileowner($session_path);
            $probe_owner = fileowner($probe);
            if ($probe_mode !== 0600 || ($dir_owner !== false && $probe_owner !== false && $dir_owner !== $probe_owner)) {
                unlink($probe);
                return false;
            }
            if (!rename($probe, $renamed)) {
                unlink($probe);
                return false;
            }
            $read_back = file_get_contents($renamed);
            $deleted = unlink($renamed);
            return $read_back === $payload && $deleted;
        }, false);
    }

    private static function runtimeOptionsAreSecure() {
        return (string)ini_get('session.use_cookies') === '1'
            && (string)ini_get('session.use_only_cookies') === '1'
            && (string)ini_get('session.use_strict_mode') === '1'
            && (string)ini_get('session.cookie_httponly') === '1';
    }

    private static function cookieOptionsAreSafe(array $params, $required_path, $secure) {
        return self::cookiePathCovers((string)($params['path'] ?? ''), $required_path)
            && (string)($params['domain'] ?? '') === ''
            && (bool)($params['secure'] ?? false) === (bool)$secure
            && !empty($params['httponly'])
            && (PHP_VERSION_ID < 70300 || self::sameSiteIsSafe((string)($params['samesite'] ?? '')));
    }

    





    private static function cookiePathCovers($effective_path, $required_path) {
        $effective = self::canonicalCookiePath($effective_path);
        $required = self::canonicalCookiePath($required_path);
        if ($effective === '' || $required === '') {
            return false;
        }
        if ($effective === '/' || $effective === $required) {
            return true;
        }

        return strpos($required, $effective . '/') === 0;
    }

    private static function canonicalCookiePath($path) {
        $path = (string)$path;
        if ($path === ''
            || $path[0] !== '/'
            || strpos($path, '//') !== false
            || preg_match('~[\x00-\x20\x7f;,?#\\\\]~', $path)
             
             
            || preg_match('~%(?:0[0-9a-f]|1[0-9a-f]|20|23|25|2c|2e|2f|3b|3f|5c|7f)~i', $path)
        ) {
            return '';
        }

        foreach (explode('/', $path) as $segment) {
            if ($segment === '.' || $segment === '..') {
                return '';
            }
        }

        return $path === '/' ? '/' : rtrim($path, '/');
    }

    private static function sameSiteIsSafe($same_site) {
        $same_site = strtolower((string)$same_site);
         
         
        return $same_site === 'lax' || $same_site === 'strict';
    }

    private static function detachForeignSession() {
        $closed = (bool)\EasyChat\Library\SafeRuntime::call(function () {
            return session_write_close();
        }, false);
        if (!$closed && function_exists('session_abort')) {
            \EasyChat\Library\SafeRuntime::call(function () {
                return session_abort();
            }, false);
        }
        if (session_status() !== PHP_SESSION_ACTIVE) {
            \EasyChat\Library\RequestContext::replaceSession([]);
            return true;
        }
        return false;
    }

    private static function cookiePath() {
        $server = \EasyChat\Library\RequestContext::server();
        $script = isset($server['SCRIPT_NAME']) ? str_replace('\\', '/', (string)$server['SCRIPT_NAME']) : '';
        $dir = $script !== '' ? dirname($script) : '/';
        $dir = str_replace('\\', '/', (string)$dir);

        if ($dir === '' || $dir === '.' || $dir === '/') {
            return '/';
        }

        return rtrim($dir, '/') . '/';
    }

    private static function isHttps() {
        $server = \EasyChat\Library\RequestContext::server();
        $first = function ($key) use ($server) {
            $value = isset($server[$key]) ? (string)$server[$key] : '';
            $parts = explode(',', $value);
            return strtolower(trim($parts[0]));
        };

        $direct_https = ($first('HTTPS') !== '' && $first('HTTPS') !== 'off' && $first('HTTPS') !== '0')
            || (string)($server['SERVER_PORT'] ?? '') === '443'
            || $first('REQUEST_SCHEME') === 'https';
        if ($direct_https) {
            return true;
        }

         
         
        if (!self::isTrustedProxyRequest($server)) {
            return false;
        }

        return $first('HTTP_X_FORWARDED_PROTO') === 'https'
            || $first('HTTP_X_FORWARDED_SCHEME') === 'https'
            || in_array($first('HTTP_X_FORWARDED_SSL'), ['on', '1', 'https'], true)
            || in_array($first('HTTP_FRONT_END_HTTPS'), ['on', '1', 'https'], true)
            || $first('HTTP_X_FORWARDED_PORT') === '443'
            || strpos(strtolower((string)($server['HTTP_CF_VISITOR'] ?? '')), 'https') !== false;
    }

    private static function isTrustedProxyRequest(array $server) {
        if (defined('EASYCHAT_TRUST_PROXY_HEADERS')) {
            return EASYCHAT_TRUST_PROXY_HEADERS === true || EASYCHAT_TRUST_PROXY_HEADERS === 1 || EASYCHAT_TRUST_PROXY_HEADERS === '1';
        }

        $remote_addr = trim((string)($server['REMOTE_ADDR'] ?? ''));
        if ($remote_addr === '127.0.0.1' || $remote_addr === '::1') {
            return true;
        }

        if (!defined('EASYCHAT_TRUSTED_PROXY_IPS')) {
            return false;
        }
        $trusted = is_array(EASYCHAT_TRUSTED_PROXY_IPS)
            ? EASYCHAT_TRUSTED_PROXY_IPS
            : explode(',', (string)EASYCHAT_TRUSTED_PROXY_IPS);
        foreach ($trusted as $ip) {
            if ($remote_addr !== '' && hash_equals(trim((string)$ip), $remote_addr)) {
                return true;
            }
        }
        return false;
    }
}
