cache = new DbStatementCache($dbConn); } private static function generateCode(): string { // results in a 12 char code return Base32::encode(random_bytes(7)); } public function getToken( UserInfo|string|null $userInfo = null, IPAddress|string|null $remoteAddr = null, ?string $verifyCode = null, ?bool $isUnused = null ): RecoveryTokenInfo { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($remoteAddr instanceof IPAddress) $remoteAddr = (string)$remoteAddr; $hasUserInfo = $userInfo !== null; $hasRemoteAddr = $remoteAddr !== null; $hasVerifyCode = $verifyCode !== null; $hasIsUnused = $isUnused !== null; if(!$hasUserInfo && !$hasRemoteAddr && !$hasVerifyCode) throw new InvalidArgumentException('At least one argument must be correctly specified.'); if($hasIsUnused && $hasVerifyCode) throw new InvalidArgumentException('You may not specify $isUnused and $verifyCode at the same time.'); $args = 0; $query = 'SELECT user_id, INET6_NTOA(reset_ip), UNIX_TIMESTAMP(reset_requested), verification_code FROM msz_users_password_resets'; if($hasUserInfo) { ++$args; $query .= ' WHERE user_id = ?'; } if($hasRemoteAddr) $query .= sprintf(' %s reset_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE'); if($hasIsUnused) $query .= sprintf(' %s verification_code %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $isUnused ? 'IS NOT' : 'IS'); elseif($hasVerifyCode) $query .= sprintf(' %s verification_code = ?', ++$args > 1 ? 'AND' : 'WHERE'); $args = 0; $stmt = $this->cache->get($query); if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasRemoteAddr) $stmt->addParameter(++$args, $remoteAddr); if($hasVerifyCode) $stmt->addParameter(++$args, $verifyCode); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Recovery token not found.'); return new RecoveryTokenInfo($result); } public function createToken( UserInfo|string $userInfo, IPAddress|string $remoteAddr ): RecoveryTokenInfo { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($remoteAddr instanceof IPAddress) $remoteAddr = (string)$remoteAddr; $verifyCode = self::generateCode(); $stmt = $this->cache->get('INSERT INTO msz_users_password_resets (user_id, reset_ip, verification_code) VALUES (?, INET6_ATON(?), ?)'); $stmt->addParameter(1, $userInfo); $stmt->addParameter(2, $remoteAddr); $stmt->addParameter(3, $verifyCode); $stmt->execute(); return $this->getToken(verifyCode: $verifyCode); } public function invalidateToken(RecoveryTokenInfo $tokenInfo): void { if(!$tokenInfo->hasCode()) return; $stmt = $this->cache->get('UPDATE msz_users_password_resets SET verification_code = NULL WHERE verification_code = ? AND user_id = ?'); $stmt->addParameter(1, $tokenInfo->getCode()); $stmt->addParameter(2, $tokenInfo->getUserId()); $stmt->execute(); } }