ytkns/src/UserSession.php
2020-06-10 16:03:13 +00:00

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()
');
}
}