connection = $database->getConnection(); return true; } catch (Throwable $exception) { return false; } } public function close(): bool { $this->connection = null; return true; } public function read(string $id): string { if ($this->connection === null) { if (!$this->open('', '')) { return ''; } } $statement = $this->connection->prepare( 'SELECT data, ip_address FROM sessions WHERE id = :id' ); $statement->bindValue(':id', $id, PDO::PARAM_STR); $statement->execute(); $row = $statement->fetch(PDO::FETCH_ASSOC); if ($row === false) { return ''; } $currentIp = $_SERVER['REMOTE_ADDR'] ?? ''; if (!empty($row['ip_address']) && $row['ip_address'] !== $currentIp) { $this->ipMismatch = true; return ''; } $this->updateAccessAndIp($id, $currentIp); return (string) $row['data']; } public function write(string $id, string $data): bool { if ($this->connection === null) { if (!$this->open('', '')) { return false; } } $currentIp = $_SERVER['REMOTE_ADDR'] ?? ''; $access = time(); $userId = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : null; $createdAt = date('Y-m-d H:i:s'); $statement = $this->connection->prepare( 'INSERT INTO sessions (id, access, data, user_id, ip_address, created_at) VALUES (:id, :access, :data, :user_id, :ip_address, :created_at) ON DUPLICATE KEY UPDATE access = VALUES(access), data = VALUES(data), user_id = VALUES(user_id), ip_address = VALUES(ip_address)' ); $statement->bindValue(':id', $id, PDO::PARAM_STR); $statement->bindValue(':access', $access, PDO::PARAM_INT); $statement->bindValue(':data', $data, PDO::PARAM_STR); if ($userId === null) { $statement->bindValue(':user_id', null, PDO::PARAM_NULL); } else { $statement->bindValue(':user_id', $userId, PDO::PARAM_INT); } $statement->bindValue(':ip_address', $currentIp, PDO::PARAM_STR); $statement->bindValue(':created_at', $createdAt, PDO::PARAM_STR); try { return $statement->execute(); } catch (Throwable $e) { return false; } } public function destroy(string $id): bool { if ($this->connection === null) { return false; } $statement = $this->connection->prepare( 'DELETE FROM sessions WHERE id = :id' ); $statement->bindValue(':id', $id, PDO::PARAM_STR); return $statement->execute(); } public function gc(int $maxLifetime): int|false { if ($this->connection === null) { return false; } $statement = $this->connection->prepare( 'DELETE FROM sessions WHERE access < :time' ); $statement->bindValue(':time', time() - $maxLifetime, PDO::PARAM_INT); return $statement->execute(); } public function validateIp(string $id): bool { if ($this->connection === null) { return true; } $statement = $this->connection->prepare( 'SELECT ip_address FROM sessions WHERE id = :id' ); $statement->bindValue(':id', $id, PDO::PARAM_STR); $statement->execute(); $row = $statement->fetch(PDO::FETCH_ASSOC); $currentIp = $_SERVER['REMOTE_ADDR'] ?? ''; return empty($row['ip_address']) || $row['ip_address'] === $currentIp; } public function hasIpMismatch(): bool { return $this->ipMismatch; } private function updateAccessAndIp(string $id, string $ip): void { $statement = $this->connection->prepare( 'UPDATE sessions SET access = :access, ip_address = :ip_address WHERE id = :id' ); $statement->bindValue(':access', time(), PDO::PARAM_INT); $statement->bindValue(':ip_address', $ip, PDO::PARAM_STR); $statement->bindValue(':id', $id, PDO::PARAM_STR); $statement->execute(); } } $handler = new MySqlSessionHandler(); session_set_save_handler($handler, true); session_start(); // Only enforce IP mismatch for authenticated sessions (user_id present) if (!empty($_SESSION['user_id']) && $handler->hasIpMismatch()) { session_unset(); session_destroy(); setcookie(session_name(), '', time() - 42000, '/'); header('Location: /index.php'); exit; }