session_id < 1 ? -1 : $this->session_id; } public function getUserId(): int { return $this->user_id < 1 ? -1 : $this->user_id; } public function getUser(): User { if($this->user === null) $this->user = User::byId($this->getUserId()); return $this->user; } public function getToken(): string { return $this->session_key; } public function getInitialRemoteAddress(): string { return $this->session_ip; } public function getLastRemoteAddress(): string { return $this->session_ip_last ?? ''; } public function hasLastRemoteAddress(): bool { return !empty($this->session_ip_last); } public function setLastRemoteAddress(string $remoteAddr): self { $this->session_ip_last = $remoteAddr; return $this; } public function getUserAgent(): string { return $this->session_user_agent; } public function getClientInfo(): ClientInfo { return ClientInfo::decode($this->session_client_info); } public function getCountry(): string { return $this->session_country; } public function getCountryName(): string { return get_country_name($this->getCountry()); } public function getCreatedTime(): int { return $this->session_created === null ? -1 : $this->session_created; } public function getActiveTime(): int { return $this->session_active === null ? -1 : $this->session_active; } public function hasActiveTime(): bool { return $this->session_active !== null; } public function setActiveTime(int $timestamp): self { if($timestamp > $this->session_active) $this->session_active = $timestamp; return $this; } public function getExpiresTime(): int { return $this->session_expires === null ? -1 : $this->session_expires; } public function setExpiresTime(int $timestamp): self { $this->session_expires = $timestamp; return $this; } public function hasExpired(): bool { return $this->getExpiresTime() <= time(); } public function shouldBumpExpire(): bool { return boolval($this->session_expires_bump); } public function bump(string $remoteAddr, int $timestamp = -1): void { if($timestamp < 0) $timestamp = time(); $this->setActiveTime($timestamp) ->setLastRemoteAddress($remoteAddr); if($this->shouldBumpExpire()) $this->setExpiresTime($timestamp + self::LIFETIME); $this->update(); } public function delete(): void { DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `session_id` = :session') ->bind('session', $this->getId()) ->execute(); } public static function purgeUser(User $user): void { DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `user_id` = :user') ->bind('user', $user->getId()) ->execute(); } public function setCurrent(): void { self::$localSession = $this; } public static function unsetCurrent(): void { self::$localSession = null; } public static function getCurrent(): ?self { return self::$localSession; } public static function hasCurrent(): bool { return self::$localSession !== null; } public static function generateToken(): string { return bin2hex(random_bytes(self::TOKEN_SIZE / 2)); } public function update(): void { DB::prepare( 'UPDATE `' . DB::PREFIX . self::TABLE . '`' . ' SET `session_active` = FROM_UNIXTIME(:active), `session_ip_last` = INET6_ATON(:remote_addr), `session_expires` = FROM_UNIXTIME(:expires)' . ' WHERE `session_id` = :session' ) ->bind('active', $this->session_active) ->bind('remote_addr', $this->session_ip_last) ->bind('expires', $this->session_expires) ->bind('session', $this->session_id) ->execute(); } public static function create( User $user, string $remoteAddr, string $countryCode, ?string $userAgent = null, ?ClientInfo $clientInfo = null ): self { $userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? ''; $clientInfo ??= ClientInfo::parse($_SERVER); $token = self::generateToken(); $sessionId = DB::prepare( 'INSERT INTO `' . DB::PREFIX . self::TABLE . '`' . ' (`user_id`, `session_ip`, `session_country`, `session_user_agent`, `session_client_info`, `session_key`, `session_created`, `session_expires`)' . ' VALUES (:user, INET6_ATON(:remote_addr), :country, :user_agent, :client_info, :token, NOW(), NOW() + INTERVAL :expires SECOND)' ) ->bind('user', $user->getId()) ->bind('remote_addr', $remoteAddr) ->bind('country', $countryCode) ->bind('user_agent', $userAgent) ->bind('client_info', $clientInfo->encode()) ->bind('token', $token) ->bind('expires', self::LIFETIME) ->executeGetId(); if($sessionId < 1) throw new RuntimeException('Failed to create new session.'); return self::byId($sessionId); } private static function countQueryBase(): string { return sprintf(self::QUERY_SELECT, 'COUNT(*)'); } public static function countAll(?User $user = null): int { $getCount = DB::prepare( self::countQueryBase() . ($user === null ? '' : ' WHERE `user_id` = :user') ); if($user !== null) $getCount->bind('user', $user->getId()); return (int)$getCount->fetchColumn(); } private static function byQueryBase(): string { return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE)); } public static function byId(int $sessionId): self { $session = DB::prepare(self::byQueryBase() . ' WHERE `session_id` = :session_id') ->bind('session_id', $sessionId) ->fetchObject(self::class); if(!$session) throw new RuntimeException('Could not find a session with that ID.'); return $session; } public static function byToken(string $token): self { $session = DB::prepare(self::byQueryBase() . ' WHERE `session_key` = :token') ->bind('token', $token) ->fetchObject(self::class); if(!$session) throw new RuntimeException('Could not find a session with that token.'); return $session; } public static function all(?Pagination $pagination = null, ?User $user = null): array { $sessionsQuery = self::byQueryBase() . ($user === null ? '' : ' WHERE `user_id` = :user') . ' ORDER BY `session_created` DESC'; if($pagination !== null) $sessionsQuery .= ' LIMIT :range OFFSET :offset'; $getSessions = DB::prepare($sessionsQuery); if($user !== null) $getSessions->bind('user', $user->getId()); if($pagination !== null) $getSessions->bind('range', $pagination->getRange()) ->bind('offset', $pagination->getOffset()); return $getSessions->fetchObjects(self::class); } }