150 lines
4.7 KiB
PHP
150 lines
4.7 KiB
PHP
<?php
|
|
namespace YTKNS;
|
|
|
|
use Exception;
|
|
|
|
final class UserSessionNotFoundException extends Exception {};
|
|
final class UserSessionCreatedFailedException extends Exception {};
|
|
|
|
class UserSession {
|
|
private const TOKEN_CHARS = 'abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
private static $instance = null;
|
|
|
|
public static function instance(): ?self {
|
|
return self::$instance;
|
|
}
|
|
public function setInstance(): void {
|
|
self::$instance = $this;
|
|
}
|
|
public static function hasInstance(): bool {
|
|
return !empty(self::$instance->session_token);
|
|
}
|
|
public static function unsetInstance(): void {
|
|
self::$instance = null;
|
|
}
|
|
|
|
public function __construct() {
|
|
}
|
|
|
|
public function getUserId(): int {
|
|
return $this->user_id ?? 0;
|
|
}
|
|
public function getUser(): User {
|
|
return User::byId($this->getUserId());
|
|
}
|
|
|
|
public function getToken(): string {
|
|
return $this->session_token;
|
|
}
|
|
public function getSmallToken(int $rounds = 5, int $length = 8, int $offset = 0): string {
|
|
$token = $this->getToken();
|
|
$tokenLength = strlen($token) - $length;
|
|
|
|
for($i = 0; $i < $rounds; $i++)
|
|
$offset = ord($token[$offset]) % $tokenLength;
|
|
|
|
return str_rot13(substr($token, $offset, $length));
|
|
}
|
|
|
|
public function getCreated(): int {
|
|
return $this->session_created ?? 0;
|
|
}
|
|
|
|
public function getExpires(): int {
|
|
return $this->session_expires ?? 0;
|
|
}
|
|
|
|
public function getBump(): bool {
|
|
return $this->session_bump ?? false;
|
|
}
|
|
|
|
public function getFirstIp(): string {
|
|
return $this->session_ip_first ?? '';
|
|
}
|
|
|
|
public function getLastIp(): ?string {
|
|
return $this->session_ip_last ?? null;
|
|
}
|
|
|
|
public function update(): void {
|
|
$update = DB::prepare('
|
|
UPDATE `ytkns_users_sessions`
|
|
SET `session_expires` = IF(:bump, NOW() + INTERVAL 1 MONTH, `session_expires`),
|
|
`session_ip_last` = INET6_ATON(:ip),
|
|
`session_used` = NOW()
|
|
WHERE `session_token` = :token
|
|
');
|
|
$update->bindValue('bump', $this->getBump() ? 1 : 0);
|
|
$update->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
|
$update->bindValue('token', $this->getToken());
|
|
$update->execute();
|
|
}
|
|
|
|
public function destroy(): void {
|
|
$destroy = DB::prepare('
|
|
DELETE FROM `ytkns_users_sessions`
|
|
WHERE `session_token` = :token
|
|
');
|
|
$destroy->bindValue('token', $this->getToken());
|
|
$destroy->execute();
|
|
}
|
|
|
|
public static function generateToken(int $length = 64): string {
|
|
$token = random_bytes($length);
|
|
$chars = strlen(self::TOKEN_CHARS);
|
|
|
|
for($i = 0; $i < $length; $i++)
|
|
$token[$i] = self::TOKEN_CHARS[ord($token[$i]) % $chars];
|
|
|
|
return $token;
|
|
}
|
|
|
|
public static function create(User $user, bool $bump = false): self {
|
|
$token = self::generateToken();
|
|
$create = DB::prepare('
|
|
INSERT INTO `ytkns_users_sessions` (
|
|
`user_id`, `session_token`, `session_ip_first`, `session_bump`
|
|
) VALUES (
|
|
:user, :token, INET6_ATON(:ip), :bump
|
|
)
|
|
');
|
|
$create->bindValue('user', $user->getId());
|
|
$create->bindValue('token', $token);
|
|
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
|
$create->bindValue('bump', $bump ? 1 : 0);
|
|
$create->execute();
|
|
|
|
try {
|
|
return self::byToken($token);
|
|
} catch(UserSessionNotFoundException $ex) {
|
|
throw new UserSessionCreatedFailedException;
|
|
}
|
|
}
|
|
|
|
public static function byToken(string $token): self {
|
|
$getSession = DB::prepare('
|
|
SELECT `user_id`, `session_token`, `session_bump`,
|
|
UNIX_TIMESTAMP(`session_created`) AS `session_created`,
|
|
UNIX_TIMESTAMP(`session_expires`) AS `session_expires`,
|
|
INET6_NTOA(`session_ip_first`) AS `session_ip_first`,
|
|
INET6_NTOA(`session_ip_last`) AS `session_ip_last`
|
|
FROM `ytkns_users_sessions`
|
|
WHERE `session_token` = :token
|
|
');
|
|
$getSession->bindValue('token', $token);
|
|
$session = $getSession->execute() ? $getSession->fetchObject(self::class) : false;
|
|
|
|
if(!$session)
|
|
throw new UserSessionNotFoundException;
|
|
|
|
return $session;
|
|
}
|
|
|
|
public static function purge(): void {
|
|
DB::exec('
|
|
DELETE FROM `ytkns_users_sessions`
|
|
WHERE `session_expires` <= NOW()
|
|
');
|
|
}
|
|
}
|