<commit_before><?php

declare(strict_types=1);

/*
 * This file is part of the Valkyrja Framework package.
 *
 * (c) <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Valkyrja\Auth\Repositories;

use Throwable;
use Valkyrja\Auth\AuthenticatedUsers;
use Valkyrja\Auth\Constants\HeaderValue;
use Valkyrja\Auth\Exceptions\InvalidAuthenticationException;
use Valkyrja\Auth\Exceptions\MissingTokenizableUserRequiredFieldsException;
use Valkyrja\Auth\Exceptions\TokenizationException;
use Valkyrja\Auth\TokenizableUser;
use Valkyrja\Auth\TokenizedRepository as Contract;
use Valkyrja\Auth\User;
use Valkyrja\Http\Constants\Header;
use Valkyrja\Http\Request;

/**
 * Abstract Class TokenizedRepository.
 *
 * @author <NAME>
 *
 * @property TokenizableUser        $user
 * @property TokenizableUser|string $userEntityName
 */
abstract class TokenizedRepository extends Repository implements Contract
{
    /**
     * The token.
     *
     * @var string
     */
    protected string $token;

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setUser(User $user): static
    {
        parent::setUser($user);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setUsers(AuthenticatedUsers $users): static
    {
        parent::setUsers($users);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function authenticate(User $user): static
    {
        parent::authenticate($user);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromSession(): static
    {
        $token = $this->getTokenFromSession();

        return $this->authenticateFromToken($token);
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromRequest(Request $request): static
    {
        $token = $this->getTokenFromRequest($request);

        return $this->authenticateFromToken($token);
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setSession(): static
    {
        $this->session->set($this->user::getTokenSessionId(), $this->getToken());

        return $this;
    }

    /**
     * Get the user token.
     *
     * @throws TokenizationException
     *
     * @return string
     */
    public function getToken(): string
    {
        return $this->token ??= $this->getFreshToken();
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromToken(string $token): static
    {
        $user = $this->getUserFromToken($token);

        return $this->authenticateWithUser($user);
    }

    /**
     * Try tokenizing the users.
     *
     * @param AuthenticatedUsers $users The users
     *
     * @throws TokenizationException
     *
     * @return string
     */
    protected function tryTokenizingUsers(AuthenticatedUsers $users): string
    {
        try {
            return $this->tokenizeUsers($users);
        } catch (Throwable $exception) {
            throw new TokenizationException($exception->getMessage(), $exception->getCode(), $exception);
        }
    }

    /**
     * Get the required fields.
     *
     * @return string[]
     */
    protected function getRequiredFields(): array
    {
        $requiredFields = [
            $this->userEntityName::getIdField(),
        ];

        // If the always authenticate flag is on in the config we need the password and authentication fields to be part
        // of the tokenized user
        if ($this->config['alwaysAuthenticate']) {
            $requiredFields[] = $this->userEntityName::getPasswordField();

            $requiredFields = array_merge($requiredFields, $this->user::getAuthenticationFields());
        }

        return $requiredFields;
    }

    /**
     * Ensure required fields for tokenization.
     *
     * @param AuthenticatedUsers $users The users
     *
     * @return void
     */
    protected function ensureRequiredFieldsForTokenization(AuthenticatedUsers $users): void
    {
        // Required fields that should exist within the tokenized user
        $requiredFields = $this->getRequiredFields();

        /** @var TokenizableUser $userFromCollection */
        foreach ($users->all() as $key => $userFromCollection) {
            $userAsTokenizableArray = $collectionAsArray['users'][$key] = $userFromCollection->asTokenizableArray();

            foreach ($requiredFields as $requiredField) {
                if (! isset($userAsTokenizableArray[$requiredField])) {
                    $entityName = $this->userEntityName;

                    throw new MissingTokenizableUserRequiredFieldsException("Required field `$requiredField` is not being returned in $entityName::asTokenizableArray()");
                }
            }
        }
    }

    /**
     * Get the user token from session.
     *
     * @return string|null
     */
    protected function getTokenFromSession(): ?string
    {
        return $this->session->get($this->userEntityName::getTokenSessionId());
    }

    /**
     * Get the token from a request.
     *
     * @param Request $request The request
     *
     * @return string
     */
    protected function getTokenFromRequest(Request $request): string
    {
        [$bearer, $token] = explode(' ', $request->getHeaderLine(Header::AUTHORIZATION));

        if ($bearer !== HeaderValue::BEARER) {
            throw new InvalidAuthenticationException('Invalid token structure.');
        }

        return $token;
    }

    /**
     * Store the user token in session.
     *
     * @param string|null $token [optional] The token to store
     *
     * @throws TokenizationException
     *
     * @return static
     */
    protected function storeToken(string $token = null): static
    {
        $this->session->set($this->user::getTokenSessionId(), $token ?? $this->getToken());

        return $this;
    }

    /**
     * Get a user from a token.
     *
     * @param string|null $token [optional] The token
     *
     * @throws InvalidAuthenticationException
     *
     * @return User
     */
    protected function getUserFromToken(string $token = null): User
    {
        try {
            $users = $this->tryUnTokenizingUsers($token);
        } catch (Throwable $exception) {
            $this->resetAfterUnAuthentication();

            throw new InvalidAuthenticationException('Invalid user token.', $exception->getCode(), $exception);
        }

        $this->users = $users;

        return $users->getCurrent();
    }

    /**
     * Attempt to get users from token.
     *
     * @param string $token The token
     *
     * @throws TokenizationException
     *
     * @return AuthenticatedUsers
     */
    protected function tryUnTokenizingUsers(string $token): AuthenticatedUsers
    {<commit_msg>Always ensure a current user exists before blindly assuming it does.<commit_after><?php

declare(strict_types=1);

/*
 * This file is part of the Valkyrja Framework package.
 *
 * (c) <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Valkyrja\Auth\Repositories;

use Throwable;
use Valkyrja\Auth\AuthenticatedUsers;
use Valkyrja\Auth\Constants\HeaderValue;
use Valkyrja\Auth\Exceptions\InvalidAuthenticationException;
use Valkyrja\Auth\Exceptions\InvalidCurrentAuthenticationException;
use Valkyrja\Auth\Exceptions\MissingTokenizableUserRequiredFieldsException;
use Valkyrja\Auth\Exceptions\TokenizationException;
use Valkyrja\Auth\TokenizableUser;
use Valkyrja\Auth\TokenizedRepository as Contract;
use Valkyrja\Auth\User;
use Valkyrja\Http\Constants\Header;
use Valkyrja\Http\Request;

/**
 * Abstract Class TokenizedRepository.
 *
 * @author <NAME>
 *
 * @property TokenizableUser        $user
 * @property TokenizableUser|string $userEntityName
 */
abstract class TokenizedRepository extends Repository implements Contract
{
    /**
     * The token.
     *
     * @var string
     */
    protected string $token;

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setUser(User $user): static
    {
        parent::setUser($user);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setUsers(AuthenticatedUsers $users): static
    {
        parent::setUsers($users);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function authenticate(User $user): static
    {
        parent::authenticate($user);

        $this->resetToken();

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromSession(): static
    {
        $token = $this->getTokenFromSession();

        return $this->authenticateFromToken($token);
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromRequest(Request $request): static
    {
        $token = $this->getTokenFromRequest($request);

        return $this->authenticateFromToken($token);
    }

    /**
     * @inheritDoc
     *
     * @throws TokenizationException
     */
    public function setSession(): static
    {
        $this->session->set($this->user::getTokenSessionId(), $this->getToken());

        return $this;
    }

    /**
     * Get the user token.
     *
     * @throws TokenizationException
     *
     * @return string
     */
    public function getToken(): string
    {
        return $this->token ??= $this->getFreshToken();
    }

    /**
     * @inheritDoc
     */
    public function authenticateFromToken(string $token): static
    {
        $user = $this->getUserFromToken($token);

        return $this->authenticateWithUser($user);
    }

    /**
     * Try tokenizing the users.
     *
     * @param AuthenticatedUsers $users The users
     *
     * @throws TokenizationException
     *
     * @return string
     */
    protected function tryTokenizingUsers(AuthenticatedUsers $users): string
    {
        try {
            return $this->tokenizeUsers($users);
        } catch (Throwable $exception) {
            throw new TokenizationException($exception->getMessage(), $exception->getCode(), $exception);
        }
    }

    /**
     * Get the required fields.
     *
     * @return string[]
     */
    protected function getRequiredFields(): array
    {
        $requiredFields = [
            $this->userEntityName::getIdField(),
        ];

        // If the always authenticate flag is on in the config we need the password and authentication fields to be part
        // of the tokenized user
        if ($this->config['alwaysAuthenticate']) {
            $requiredFields[] = $this->userEntityName::getPasswordField();

            $requiredFields = array_merge($requiredFields, $this->user::getAuthenticationFields());
        }

        return $requiredFields;
    }

    /**
     * Ensure required fields for tokenization.
     *
     * @param AuthenticatedUsers $users The users
     *
     * @return void
     */
    protected function ensureRequiredFieldsForTokenization(AuthenticatedUsers $users): void
    {
        // Required fields that should exist within the tokenized user
        $requiredFields = $this->getRequiredFields();

        /** @var TokenizableUser $userFromCollection */
        foreach ($users->all() as $key => $userFromCollection) {
            $userAsTokenizableArray = $collectionAsArray['users'][$key] = $userFromCollection->asTokenizableArray();

            foreach ($requiredFields as $requiredField) {
                if (! isset($userAsTokenizableArray[$requiredField])) {
                    $entityName = $this->userEntityName;

                    throw new MissingTokenizableUserRequiredFieldsException("Required field `$requiredField` is not being returned in $entityName::asTokenizableArray()");
                }
            }
        }
    }

    /**
     * Get the user token from session.
     *
     * @return string|null
     */
    protected function getTokenFromSession(): ?string
    {
        return $this->session->get($this->userEntityName::getTokenSessionId());
    }

    /**
     * Get the token from a request.
     *
     * @param Request $request The request
     *
     * @return string
     */
    protected function getTokenFromRequest(Request $request): string
    {
        [$bearer, $token] = explode(' ', $request->getHeaderLine(Header::AUTHORIZATION));

        if ($bearer !== HeaderValue::BEARER) {
            throw new InvalidAuthenticationException('Invalid token structure.');
        }

        return $token;
    }

    /**
     * Store the user token in session.
     *
     * @param string|null $token [optional] The token to store
     *
     * @throws TokenizationException
     *
     * @return static
     */
    protected function storeToken(string $token = null): static
    {
        $this->session->set($this->user::getTokenSessionId(), $token ?? $this->getToken());

        return $this;
    }

    /**
     * Get a user from a token.
     *
     * @param string|null $token [optional] The token
     *
     * @throws InvalidAuthenticationException
     *
     * @return User
     */
    protected function getUserFromToken(string $token = null): User
    {
        try {
            $users = $this->tryUnTokenizingUsers($token);
        } catch (Throwable $exception) {
            $this->resetAfterUnAuthentication();

            throw new InvalidAuthenticationException('Invalid user token.', $exception->getCode(), $exception);
        }

        $this->users = $users;

        $current = $this->users->getCurrent();

        if (! $current) {
            throw new InvalidCurrentAuthenticationException('No current authenticated user.');
        }

        return $current;
    }

    /**
     * Attempt to get users from token.
     *
     * @param string $token The token
     *
     * @throws TokenizationException
     *
     * @return AuthenticatedUsers
     */
    protected function tryUnTokenizingUsers(string $token): AuthenticatedUsers
    {
{'fraction_non_alphanumeric': 0.11425206124852769, 'fraction_numerical': 0.00013857132959190744, 'mean_word_length': 2.4066556525843756, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 92, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1367331', 'n_tokens_mistral': 3937, 'n_tokens_neox': 3833, 'n_words': 906}