Standardize settings file naming and relocate documentation files Fix code quality violations from rsx:check Reorganize user_management directory into logical subdirectories Move Quill Bundle to core and align with Tom Select pattern Simplify Site Settings page to focus on core site information Complete Phase 5: Multi-tenant authentication with login flow and site selection Add route query parameter rule and synchronize filename validation logic Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs Implement filename convention rule and resolve VS Code auto-rename conflict Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns Implement RPC server architecture for JavaScript parsing WIP: Add RPC server infrastructure for JS parsing (partial implementation) Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation Add JQHTML-CLASS-01 rule and fix redundant class names Improve code quality rules and resolve violations Remove legacy fatal error format in favor of unified 'fatal' error type Filter internal keys from window.rsxapp output Update button styling and comprehensive form/modal documentation Add conditional fly-in animation for modals Fix non-deterministic bundle compilation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
805 lines
21 KiB
PHP
Executable File
805 lines
21 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Core\Session;
|
|
|
|
use RuntimeException;
|
|
use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
|
|
use App\RSpade\Core\Manifest\Manifest;
|
|
use App\RSpade\Core\Models\Login_User_Model;
|
|
use App\RSpade\Core\Models\Site_Model;
|
|
use App\RSpade\Core\Models\User_Model;
|
|
|
|
/**
|
|
* Session model - handles both authentication sessions and static session management
|
|
*
|
|
* This class serves dual purposes:
|
|
* 1. As a Laravel Eloquent model for the sessions table
|
|
* 2. As a static interface for session management (similar to RS3 design)
|
|
*
|
|
* The session represents a unique browser session with persistent authentication.
|
|
* Sessions are always persistent (365 days) - no "remember me" option.
|
|
*
|
|
* @FILE-SUBCLASS-01-EXCEPTION Class intentionally named Session instead of Session_Model
|
|
* to maintain compatibility with static method calls like Session::init(). The developer will
|
|
* almost never be interacting with a session orm record directly, so the term Session_Model
|
|
* doesnt have much meaning.
|
|
*
|
|
* @property int $id
|
|
* @property bool $active
|
|
* @property int $site_id
|
|
* @property int $login_user_id
|
|
* @property int $experience_id
|
|
* @property string $session_token
|
|
* @property string $csrf_token
|
|
* @property string $ip_address
|
|
* @property string $user_agent
|
|
* @property \Carbon\Carbon $last_active
|
|
* @property int $version
|
|
* @property \Carbon\Carbon $created_at
|
|
* @property \Carbon\Carbon $updated_at
|
|
*/
|
|
/**
|
|
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
|
* Generated on: 2025-11-04 07:18:11
|
|
* Table: sessions
|
|
*
|
|
* @property int $id
|
|
* @property bool $active
|
|
* @property int $site_id
|
|
* @property int $login_user_id
|
|
* @property mixed $session_token
|
|
* @property mixed $csrf_token
|
|
* @property mixed $ip_address
|
|
* @property mixed $user_agent
|
|
* @property string $last_active
|
|
* @property int $version
|
|
* @property string $created_at
|
|
* @property string $updated_at
|
|
* @property int $created_by
|
|
* @property int $updated_by
|
|
* @property int $experience_id
|
|
*
|
|
* @mixin \Eloquent
|
|
*/
|
|
class Session extends Rsx_System_Model_Abstract
|
|
{
|
|
// Enum definitions (required by abstract parent)
|
|
public static $enums = [];
|
|
|
|
// Static session management properties
|
|
private static $_session = null;
|
|
|
|
private static $_site = null;
|
|
|
|
private static $_login_user = null; // Authentication identity (Login_User_Model)
|
|
|
|
private static $_user = null; // Site-specific user (User_Model)
|
|
|
|
private static $_session_token = null;
|
|
|
|
private static $_has_init = false;
|
|
|
|
private static $_has_activate = false;
|
|
|
|
private static $_has_set_cookie = false;
|
|
|
|
// Experience and request-scoped overrides
|
|
private static $_experience_id = 0;
|
|
|
|
private static $_request_site_id_override = null;
|
|
|
|
// CLI mode properties (static-only, no database)
|
|
private static $_cli_site_id = null;
|
|
|
|
private static $_cli_login_user_id = null; // Authentication identity ID
|
|
|
|
private static $_cli_user_id = null; // Site-specific user ID
|
|
|
|
private static $_cli_experience_id = 0;
|
|
|
|
/**
|
|
* The table associated with the model
|
|
* @var string
|
|
*/
|
|
protected $table = 'sessions';
|
|
|
|
/**
|
|
* The attributes that should be cast
|
|
* @var array
|
|
*/
|
|
protected $casts = [
|
|
'active' => 'boolean',
|
|
'site_id' => 'integer',
|
|
'login_user_id' => 'integer',
|
|
'experience_id' => 'integer',
|
|
'version' => 'integer',
|
|
'last_active' => 'datetime',
|
|
];
|
|
|
|
/**
|
|
* Columns that should never be exported to JavaScript
|
|
* @var array
|
|
*/
|
|
protected $neverExport = [
|
|
'session_token',
|
|
'csrf_token',
|
|
'ip_address',
|
|
];
|
|
|
|
/**
|
|
* Check if running in CLI mode
|
|
* @return bool
|
|
*/
|
|
private static function __is_cli(): bool
|
|
{
|
|
return php_sapi_name() === 'cli';
|
|
}
|
|
|
|
/**
|
|
* Check if requester is a playwright fpc client
|
|
* @return bool
|
|
*/
|
|
private static function __is_fpc_client(): bool
|
|
{
|
|
return isset($_SERVER['HTTP_X_RSPADE_FPC_CLIENT']) && $_SERVER['HTTP_X_RSPADE_FPC_CLIENT'] === '1';
|
|
}
|
|
|
|
/**
|
|
* Initialize session from cookie or request
|
|
* Loads existing session but does not create new one
|
|
* Filters by current experience_id to support multiple authentication realms
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
public static function init(): void
|
|
{
|
|
if (self::$_has_init) {
|
|
return;
|
|
}
|
|
self::$_has_init = true;
|
|
|
|
// CLI mode: do nothing
|
|
if (self::__is_cli() || self::__is_fpc_client()) {
|
|
return;
|
|
}
|
|
|
|
Manifest::init();
|
|
|
|
// Try to get session token from cookie or request
|
|
$session_token = $_COOKIE['rsx'] ?? null;
|
|
|
|
if (empty($session_token)) {
|
|
self::$_session = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// Load session for CURRENT EXPERIENCE only
|
|
// This allows same cookie to have different sessions per experience
|
|
$session = static::where('session_token', $session_token)
|
|
->where('active', true)
|
|
->where('experience_id', self::$_experience_id)
|
|
->first();
|
|
|
|
if (!$session) {
|
|
self::$_session = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// Update last activity (but don't save immediately to avoid version conflicts)
|
|
$session->last_active = now();
|
|
|
|
// We'll let the session save happen later if needed (e.g. in set_user)
|
|
// For simple page loads, we can update last_active in a separate query
|
|
static::where('id', $session->id)->update(['last_active' => now()]);
|
|
|
|
// Reload the session to ensure we have the latest version
|
|
$session = static::find($session->id);
|
|
|
|
self::$_session_token = $session_token;
|
|
self::$_session = $session;
|
|
|
|
self::_set_cookie();
|
|
}
|
|
|
|
/**
|
|
* Activate session - creates new one if needed
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
private static function __activate(): void
|
|
{
|
|
if (self::$_has_activate) {
|
|
return;
|
|
}
|
|
self::$_has_activate = true;
|
|
|
|
// CLI & FPC mode: do nothing
|
|
if (self::__is_cli() || self::__is_fpc_client()) {
|
|
return;
|
|
}
|
|
|
|
self::init();
|
|
|
|
// If no session exists, create one
|
|
if (empty(self::$_session)) {
|
|
// Generate cryptographically secure token
|
|
self::$_session_token = bin2hex(random_bytes(32));
|
|
|
|
// Generate CSRF token
|
|
$csrf_token = bin2hex(random_bytes(32));
|
|
|
|
$session = new static();
|
|
$session->session_token = self::$_session_token;
|
|
$session->csrf_token = $csrf_token;
|
|
$session->ip_address = self::__get_client_ip();
|
|
$session->user_agent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
|
|
$session->last_active = now();
|
|
$session->active = true;
|
|
$session->site_id = 0;
|
|
$session->experience_id = self::$_experience_id;
|
|
$session->version = 1;
|
|
|
|
$session->save();
|
|
|
|
self::$_session = $session;
|
|
self::_set_cookie();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the session cookie with security flags
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
private static function _set_cookie(): void
|
|
{
|
|
if (self::$_has_set_cookie) {
|
|
return;
|
|
}
|
|
self::$_has_set_cookie = true;
|
|
|
|
// CLI mode: do nothing
|
|
if (self::__is_cli()) {
|
|
return;
|
|
}
|
|
|
|
// Set cookie with security flags
|
|
setcookie('rsx', self::$_session_token, [
|
|
'expires' => time() + (365 * 86400), // 1 year
|
|
'path' => '/',
|
|
'domain' => '', // Current domain only
|
|
'secure' => true, // HTTPS only
|
|
'httponly' => true, // No JavaScript access
|
|
'samesite' => 'Lax', // CSRF protection
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get client IP address, handling proxies
|
|
* In CLI mode: returns "CLI"
|
|
* @return string
|
|
*/
|
|
private static function __get_client_ip(): string
|
|
{
|
|
// CLI mode: return "CLI"
|
|
if (self::__is_cli()) {
|
|
return 'CLI';
|
|
}
|
|
|
|
// Check for forwarded IP (when behind proxy/CDN)
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
|
|
return trim($ips[0]);
|
|
}
|
|
|
|
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
|
return $_SERVER['HTTP_X_REAL_IP'];
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Reset/logout the current session
|
|
* @return void
|
|
*/
|
|
public static function reset(): void
|
|
{
|
|
self::init();
|
|
|
|
if (!empty(self::$_session)) {
|
|
self::$_session->active = false;
|
|
self::$_session->save();
|
|
}
|
|
|
|
self::$_session = null;
|
|
self::$_site = null;
|
|
self::$_user = null;
|
|
self::$_has_init = false;
|
|
self::$_has_activate = false;
|
|
self::$_has_set_cookie = false;
|
|
|
|
// Clear cookie
|
|
setcookie('rsx', '', [
|
|
'expires' => time() - 3600,
|
|
'path' => '/',
|
|
'domain' => '',
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get site ID for current session
|
|
* Respects request-scoped override first (for subdomain enforcement)
|
|
* In CLI mode: returns static CLI property
|
|
* @return int
|
|
*/
|
|
public static function get_site_id(): int
|
|
{
|
|
// Request override takes precedence (subdomain enforcement)
|
|
if (self::$_request_site_id_override !== null) {
|
|
return self::$_request_site_id_override;
|
|
}
|
|
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_site_id ?? 0;
|
|
}
|
|
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return 0;
|
|
}
|
|
|
|
return self::$_session->site_id ?? 0;
|
|
}
|
|
|
|
/**
|
|
* Get site model for current session
|
|
* @return Site_Model|null
|
|
*/
|
|
public static function get_site()
|
|
{
|
|
$site_id = self::get_site_id();
|
|
|
|
if ($site_id === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_site)) {
|
|
self::$_site = Site_Model::find($site_id);
|
|
}
|
|
|
|
return self::$_site;
|
|
}
|
|
|
|
/**
|
|
* Get login user ID (authentication identity) for current session
|
|
* In CLI mode: returns static CLI property
|
|
* @return int|null
|
|
*/
|
|
public static function get_login_user_id()
|
|
{
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_login_user_id;
|
|
}
|
|
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return null;
|
|
}
|
|
|
|
return self::$_session->login_user_id;
|
|
}
|
|
|
|
/**
|
|
* Get site-specific user ID for current session
|
|
* This is the users.id (site-specific), not login_users.id
|
|
* In CLI mode: returns static CLI property
|
|
* @return int|null
|
|
*/
|
|
public static function get_user_id()
|
|
{
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_user_id;
|
|
}
|
|
|
|
$user = self::get_user();
|
|
|
|
return $user ? $user->id : null;
|
|
}
|
|
|
|
/**
|
|
* Check if user is logged in
|
|
* @return bool
|
|
*/
|
|
public static function is_logged_in(): bool
|
|
{
|
|
return !empty(self::get_login_user_id());
|
|
}
|
|
|
|
/**
|
|
* Get login user model (authentication identity) for current session
|
|
* @return Login_User_Model|null
|
|
*/
|
|
public static function get_login_user()
|
|
{
|
|
$login_user_id = self::get_login_user_id();
|
|
|
|
if (empty($login_user_id)) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_login_user)) {
|
|
self::$_login_user = Login_User_Model::find($login_user_id);
|
|
}
|
|
|
|
return self::$_login_user;
|
|
}
|
|
|
|
/**
|
|
* Get site-specific user model for current session
|
|
* @return User_Model|null
|
|
*/
|
|
public static function get_user()
|
|
{
|
|
$login_user_id = self::get_login_user_id();
|
|
$site_id = self::get_site_id();
|
|
|
|
if (empty($login_user_id) || empty($site_id)) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_user)) {
|
|
self::$_user = User_Model::where('login_user_id', $login_user_id)
|
|
->where('site_id', $site_id)
|
|
->first();
|
|
}
|
|
|
|
return self::$_user;
|
|
}
|
|
|
|
/**
|
|
* Get current session model (creates if needed)
|
|
* @return Session
|
|
*/
|
|
public static function get_session(): Session
|
|
{
|
|
self::__activate();
|
|
|
|
return self::$_session;
|
|
}
|
|
|
|
/**
|
|
* Get current session ID (creates session if needed)
|
|
* @return int
|
|
*/
|
|
public static function get_session_id(): int
|
|
{
|
|
self::__activate();
|
|
|
|
return self::$_session->id;
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token for current session
|
|
* @return string|null
|
|
*/
|
|
public static function get_csrf_token(): ?string
|
|
{
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return null;
|
|
}
|
|
|
|
return self::$_session->csrf_token;
|
|
}
|
|
|
|
/**
|
|
* Verify CSRF token
|
|
* @param string $token
|
|
* @return bool
|
|
*/
|
|
public static function verify_csrf_token(string $token): bool
|
|
{
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return false;
|
|
}
|
|
|
|
// Use constant-time comparison
|
|
return hash_equals(self::$_session->csrf_token, $token);
|
|
}
|
|
|
|
/**
|
|
* Logout current user
|
|
* @return void
|
|
*/
|
|
public static function logout(): void
|
|
{
|
|
self::set_login_user_id(null);
|
|
}
|
|
|
|
/**
|
|
* Set login user ID for current session (login/logout)
|
|
* In CLI mode: sets static CLI property only, no database
|
|
* @param int|null $login_user_id Login user ID, or null to logout
|
|
* @return void
|
|
*/
|
|
public static function set_login_user_id(?int $login_user_id): void
|
|
{
|
|
// Logout if null/0
|
|
if (empty($login_user_id)) {
|
|
// CLI mode: clear static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_login_user_id = null;
|
|
self::$_cli_user_id = null;
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
self::$_session->login_user_id = null;
|
|
self::$_session->save();
|
|
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// CLI mode: set static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_login_user_id = $login_user_id;
|
|
self::$_cli_user_id = null;
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
|
|
// Regenerate session token and CSRF token on login (prevent session fixation)
|
|
$new_token = bin2hex(random_bytes(32));
|
|
$new_csrf = bin2hex(random_bytes(32));
|
|
self::$_session->session_token = $new_token;
|
|
self::$_session->csrf_token = $new_csrf;
|
|
self::$_session->login_user_id = $login_user_id;
|
|
self::$_session->version++;
|
|
self::$_session->save();
|
|
|
|
self::$_session_token = $new_token;
|
|
self::$_has_set_cookie = false; // Force new cookie
|
|
self::_set_cookie();
|
|
|
|
// Clear cached login_user/user/site
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
// Update login user's last login timestamp
|
|
$login_user_record = self::get_login_user();
|
|
if ($login_user_record) {
|
|
$login_user_record->last_login = now();
|
|
$login_user_record->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set site for current session
|
|
* In CLI mode: sets static CLI property only, no database
|
|
* @param Site_Model|int $site Site model or site ID
|
|
* @return void
|
|
*/
|
|
/**
|
|
* Set site ID
|
|
* In CLI mode: sets static CLI property only
|
|
* @param int $site_id
|
|
* @return void
|
|
*/
|
|
public static function set_site_id(int $site_id): void
|
|
{
|
|
// CLI mode: set static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_site_id = $site_id;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
|
|
// Skip if already set
|
|
if (self::get_site_id() === $site_id) {
|
|
return;
|
|
}
|
|
|
|
self::$_session->site_id = $site_id;
|
|
self::$_session->version++;
|
|
self::$_session->save();
|
|
|
|
// Clear cached site
|
|
self::$_site = null;
|
|
}
|
|
|
|
/**
|
|
* Check if a session exists
|
|
* In CLI mode: returns true if site_id or user_id is set
|
|
* In web mode: returns true if session record exists
|
|
* @return bool
|
|
*/
|
|
public static function has_session(): bool
|
|
{
|
|
// CLI mode: check if site_id or user_id is set
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_site_id !== null || self::$_cli_user_id !== null;
|
|
}
|
|
|
|
// Web mode: init and check if session exists
|
|
self::init();
|
|
|
|
return !empty(self::$_session);
|
|
}
|
|
|
|
/**
|
|
* Get session by token (for API/external access)
|
|
* @param string $token
|
|
* @return Session|null
|
|
*/
|
|
public static function find_by_token(string $token)
|
|
{
|
|
return static::where('session_token', $token)
|
|
->where('active', true)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Clean up expired sessions (garbage collection)
|
|
* @param int $days_until_expiry
|
|
* @return int Number of sessions deleted
|
|
*/
|
|
public static function cleanup_expired(int $days_until_expiry = 365): int
|
|
{
|
|
return static::where('last_active', '<', now()->subDays($days_until_expiry))
|
|
->delete();
|
|
}
|
|
|
|
/**
|
|
* Override save to increment version on updates
|
|
* @param array $options
|
|
* @return bool
|
|
*/
|
|
public function save(array $options = []): bool
|
|
{
|
|
// Increment version on updates (but don't check for conflicts since sessions
|
|
// are single-user and shouldn't have real concurrent modifications)
|
|
if ($this->exists && $this->isDirty()) {
|
|
$this->version = ($this->version ?? 1) + 1;
|
|
}
|
|
|
|
return parent::save($options);
|
|
}
|
|
|
|
/**
|
|
* Relationship: Login User (authentication identity)
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function login_user()
|
|
{
|
|
return $this->belongsTo(Login_User_Model::class, 'login_user_id');
|
|
}
|
|
|
|
/**
|
|
* Relationship: Site
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function site()
|
|
{
|
|
return $this->belongsTo(Site_Model::class, 'site_id');
|
|
}
|
|
|
|
/**
|
|
* Relationship: Site-specific User (composite key relationship)
|
|
* Returns the User_Model that matches both login_user_id and site_id
|
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
|
*/
|
|
public function user()
|
|
{
|
|
return $this->hasOne(User_Model::class, 'login_user_id', 'login_user_id')
|
|
->where('users.site_id', '=', $this->site_id);
|
|
}
|
|
|
|
/**
|
|
* Set experience context for current request
|
|
* Determines which authentication realm we're in (default=0, staff=1, customer=2, etc.)
|
|
* Does NOT persist to database - request-scoped only
|
|
* Clears cached session/user/site since experience changed
|
|
* In CLI mode: sets static CLI property
|
|
* @param int $experience_id
|
|
* @return void
|
|
*/
|
|
public static function set_experience_id(int $experience_id): void
|
|
{
|
|
if (self::__is_cli()) {
|
|
self::$_cli_experience_id = $experience_id;
|
|
|
|
return;
|
|
}
|
|
|
|
self::$_experience_id = $experience_id;
|
|
|
|
// Clear cached data since experience changed
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
self::$_session = null;
|
|
|
|
// Force re-init with new experience context
|
|
self::$_has_init = false;
|
|
self::init();
|
|
}
|
|
|
|
/**
|
|
* Get current experience ID
|
|
* In CLI mode: returns static CLI property
|
|
* @return int
|
|
*/
|
|
public static function get_experience_id(): int
|
|
{
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_experience_id;
|
|
}
|
|
|
|
return self::$_experience_id;
|
|
}
|
|
|
|
/**
|
|
* Set site_id override for current request (subdomain enforcement)
|
|
* This overrides the database session site_id for THIS REQUEST ONLY
|
|
* Does NOT modify the session record
|
|
* Use case: User visits subdomain assigned to a tenant, enforce that tenant
|
|
* @param int $site_id
|
|
* @return void
|
|
*/
|
|
public static function set_request_site_id_override(int $site_id): void
|
|
{
|
|
self::$_request_site_id_override = $site_id;
|
|
self::$_site = null; // Clear cached site
|
|
}
|
|
|
|
/**
|
|
* Clear site_id override (return to normal session-based site_id)
|
|
* @return void
|
|
*/
|
|
public static function clear_request_site_id_override(): void
|
|
{
|
|
self::$_request_site_id_override = null;
|
|
self::$_site = null; // Clear cached site
|
|
}
|
|
|
|
/**
|
|
* Check if request has site_id override active
|
|
* @return bool
|
|
*/
|
|
public static function has_request_site_id_override(): bool
|
|
{
|
|
return self::$_request_site_id_override !== null;
|
|
}
|
|
}
|