Move small tasks from wishlist to todo, update npm packages Replace #[Auth] attributes with manual auth checks and code quality rule Remove on_jqhtml_ready lifecycle method from framework Complete ACL system with 100-based role indexing and /dev/acl tester WIP: ACL system implementation with debug instrumentation Convert rsx:check JS linting to RPC socket server Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature Reorganize wishlists: priority order, mark sublayouts complete, add email Update model_fetch docs: mark MVP complete, fix enum docs, reorganize Comprehensive documentation overhaul: clarity, compression, and critical rules Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null() Add JS ORM relationship lazy-loading and fetch array handling Add JS ORM relationship fetching and CRUD documentation Fix ORM hydration and add IDE resolution for Base_* model stubs Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework Enhance JS ORM infrastructure and add Json_Tree class name badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
796 lines
21 KiB
PHP
Executable File
796 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';
|
|
}
|
|
|
|
/**
|
|
* 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()) {
|
|
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 mode: do nothing
|
|
if (self::__is_cli()) {
|
|
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;
|
|
}
|
|
}
|