v-wordpress-plugin-updater/update-api/app/Core/SessionManager.php
nikolai@vontainment.com 178d50bf2d modified: .github/instructions/codacy.instructions.md
modified:   tests/ResponseTest.php
	modified:   tests/SessionManagerTest.php
	modified:   update-api/app/Controllers/ApiController.php
	modified:   update-api/app/Controllers/LoginController.php
	modified:   update-api/app/Core/Response.php
	modified:   update-api/app/Core/Router.php
	modified:   update-api/app/Core/SessionManager.php
	new file:   update-api/php_app.log
	modified:   update-api/public/index.php
	modified:   update-api/storage/test.sqlite
2026-03-05 19:29:37 -05:00

160 lines
4.1 KiB
PHP

<?php
// phpcs:ignoreFile PSR1.Files.SideEffects.FoundWithSymbols
/**
* Project: UpdateAPI
* Author: Vontainment <services@vontainment.com>
* License: https://opensource.org/licenses/MIT MIT License
* Link: https://vontainment.com
* Version: 4.0.0
*
* File: SessionManager.php
* Description: Centralized session management
*/
namespace App\Core;
use App\Models\BlacklistModel;
use App\Core\ErrorManager;
class SessionManager
{
/**
* Singleton instance of the SessionManager.
*/
private static ?SessionManager $instance = null;
/**
* Private constructor to prevent direct instantiation.
*/
private function __construct()
{
}
/**
* Retrieve the singleton instance.
*
* @return SessionManager
*/
public static function getInstance(): SessionManager
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Start the session with secure cookie parameters.
*
* @return void
*/
public function start(): void
{
$secureFlag = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'path' => '/',
'httponly' => true,
'secure' => $secureFlag,
'samesite' => 'Lax',
]);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
}
/**
* Destroy the current session and its data.
*
* @return void
*/
public function destroy(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION = [];
session_destroy();
}
}
/**
* Regenerate the session ID to prevent fixation.
*
* @return void
*/
public function regenerate(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
}
/**
* Retrieve a value from the session.
*
* @param string $key Session key to retrieve.
* @param mixed $default Default value if key does not exist.
* @return mixed
*/
public function get(string $key, mixed $default = null): mixed
{
return $_SESSION[$key] ?? $default;
}
/**
* Set a session value.
*
* @param string $key Session key to set.
* @param mixed $value Value to store.
* @return void
*/
public function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
/**
* Validate the current session and update timeout.
*
* @return bool True if the session is valid and user is logged in
*/
public function isValid(): bool
{
$timeoutLimit = defined('SESSION_TIMEOUT_LIMIT') ? SESSION_TIMEOUT_LIMIT : 1800;
$timeout = $this->get('timeout');
$timeoutExceeded = is_int($timeout) && (time() - $timeout > $timeoutLimit);
$userAgent = $this->get('user_agent');
$userAgentChanged = is_string($userAgent) && $userAgent !== ($_SERVER['HTTP_USER_AGENT'] ?? '');
if ($timeoutExceeded || $userAgentChanged) {
$this->destroy();
return false;
}
$this->set('timeout', time());
return $this->get('logged_in') === true;
}
/**
* Enforce that the current request comes from an authenticated user.
*
* Returns true when the session is valid and the user is authenticated.
* Returns false when the session is invalid (caller should redirect to login).
* When the remote IP is blacklisted (403), logs the attempt and returns false.
*
* @return bool True if authenticated, false otherwise.
*/
public function requireAuth(): bool
{
$ip = filter_var($_SERVER['REMOTE_ADDR'] ?? '', FILTER_VALIDATE_IP);
if ($ip && BlacklistModel::isBlacklisted($ip)) {
http_response_code(403);
ErrorManager::getInstance()->log("Blacklisted IP attempted access: $ip", 'error');
return false;
}
return $this->isValid();
}
}