Upgrade project to PHP 8.3, add PHPStan, upgrade almost every dependency (#36)

* start updating to PHP 8.3

* taking off!

Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* dropped this

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* migrate to symfonymailer

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* this is so stupid 😭

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* ah, free, at last.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* oh, Gabriel.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* now dawns thy reckoning.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* and thy gore shall GLISTEN before the temples of man.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* creature of steel.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* my gratitude upon thee for my freedom.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* but the crimes thy kind has committed against humanity

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* Upgrade PHP-CS-Fixer and do fix the codebase

* First review round (maybe I have broken something)

* are NOT forgotten.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* Enable parallel PHP-CS-Fixer runner

* PHPStan level 1

* PHPStan level 2

* PHPStan level 3

* PHPStan level 4

* PHPStan level 5

* Levels 6 and 7 takes too much effort. Generate a baseline and fix them eventually

* Resolve TODO's related to the php-mock

* Drastically reduce baseline size with the Rector

* More code modernization with help of the Rector

* Update GitLab CI

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
This commit is contained in:
Octol1ttle
2024-12-02 15:10:55 +05:00
committed by GitHub
parent 625250b367
commit 57d492da8a
356 changed files with 10531 additions and 4761 deletions

View File

@@ -6,22 +6,20 @@ namespace api\components\Tokens\Algorithms;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
final class ES256 implements AlgorithmInterface {
private string $privateKeyPath;
private ?string $privateKeyPass;
private ?Key $privateKey = null;
private ?Key $publicKey = null;
private Sha256 $signer;
private readonly Sha256 $signer;
public function __construct(string $privateKeyPath, ?string $privateKeyPass = null) {
$this->privateKeyPath = $privateKeyPath;
$this->privateKeyPass = $privateKeyPass;
public function __construct(
private readonly string $privateKeyPath,
private readonly ?string $privateKeyPass = null,
) {
$this->signer = new Sha256();
}
@@ -31,7 +29,7 @@ final class ES256 implements AlgorithmInterface {
public function getPrivateKey(): Key {
if ($this->privateKey === null) {
$this->privateKey = new Key($this->privateKeyPath, $this->privateKeyPass);
$this->privateKey = InMemory::plainText($this->privateKeyPath, $this->privateKeyPass ?? '');
}
return $this->privateKey;
@@ -40,9 +38,9 @@ final class ES256 implements AlgorithmInterface {
public function getPublicKey(): Key {
if ($this->publicKey === null) {
$privateKey = $this->getPrivateKey();
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->getContent(), $privateKey->getPassphrase() ?? '');
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->contents(), $privateKey->passphrase());
$publicPem = openssl_pkey_get_details($privateKeyOpenSSL)['key'];
$this->publicKey = new Key($publicPem);
$this->publicKey = InMemory::plainText($publicPem);
}
return $this->publicKey;

View File

@@ -6,21 +6,18 @@ namespace api\components\Tokens\Algorithms;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
class HS256 implements AlgorithmInterface {
final class HS256 implements AlgorithmInterface {
private ?InMemory $loadedKey = null;
/**
* @var string
* @param non-empty-string $key
*/
private $key;
/**
* @var Key|null
*/
private $loadedKey;
public function __construct(string $key) {
$this->key = $key;
public function __construct(
private readonly string $key,
) {
}
public function getSigner(): Signer {
@@ -37,7 +34,7 @@ class HS256 implements AlgorithmInterface {
private function loadKey(): Key {
if ($this->loadedKey === null) {
$this->loadedKey = new Key($this->key);
$this->loadedKey = InMemory::plainText($this->key);
}
return $this->loadedKey;

View File

@@ -22,11 +22,11 @@ final class AlgorithmsManager {
* @param AlgorithmInterface[] $algorithms
*/
public function __construct(array $algorithms = []) {
array_map([$this, 'add'], $algorithms);
array_map($this->add(...), $algorithms);
}
public function add(AlgorithmInterface $algorithm): self {
$id = $algorithm->getSigner()->getAlgorithmId();
$id = $algorithm->getSigner()->algorithmId();
Assert::keyNotExists($this->algorithms, $id, 'passed algorithm is already exists');
$this->algorithms[$id] = $algorithm;

View File

@@ -5,31 +5,40 @@ namespace api\components\Tokens;
use Carbon\Carbon;
use Exception;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use InvalidArgumentException;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Validator;
use ParagonIE\ConstantTime\Base64UrlSafe;
use RangeException;
use SodiumException;
use Webmozart\Assert\Assert;
use yii\base\Component as BaseComponent;
class Component extends BaseComponent {
private const PREFERRED_ALGORITHM = 'ES256';
private const string PREFERRED_ALGORITHM = 'ES256';
/**
* @var string
*/
public $privateKeyPath;
public string $privateKeyPath;
/**
* @var string|null
*/
public $privateKeyPass;
public ?string $privateKeyPass = null;
/**
* @var string
*/
public $encryptionKey;
public string $encryptionKey;
private ?AlgorithmsManager $algorithmManager = null;
@@ -39,19 +48,49 @@ class Component extends BaseComponent {
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
}
public function create(array $payloads = [], array $headers = []): Token {
/**
* @param array{
* sub?: string,
* jti?: string,
* iat?: \DateTimeImmutable,
* nbf?: \DateTimeImmutable,
* exp?: \DateTimeImmutable,
* } $payloads
* @param array $headers
*
* @throws \api\components\Tokens\AlgorithmIsNotDefinedException
*/
public function create(array $payloads = [], array $headers = []): UnencryptedToken {
$now = Carbon::now();
$builder = (new Builder())->issuedAt($now->getTimestamp());
$builder = (new Builder(new JoseEncoder(), ChainedFormatter::default()))->issuedAt($now->toDateTimeImmutable());
if (isset($payloads['sub'])) {
$builder = $builder->relatedTo($payloads['sub']);
}
if (isset($payloads['jti'])) {
$builder = $builder->identifiedBy($payloads['jti']);
}
if (isset($payloads['iat'])) {
$builder = $builder->issuedAt($payloads['iat']);
}
if (isset($payloads['nbf'])) {
$builder = $builder->canOnlyBeUsedAfter($payloads['nbf']);
}
if (isset($payloads['exp'])) {
$builder->expiresAt($payloads['exp']);
$builder = $builder->expiresAt($payloads['exp']);
}
foreach ($payloads as $claim => $value) {
$builder->withClaim($claim, $this->prepareValue($value));
if (!in_array($claim, RegisteredClaims::ALL, true)) { // Registered claims are handled by the if-chain above
$builder = $builder->withClaim($claim, $this->prepareValue($value));
}
}
foreach ($headers as $claim => $value) {
$builder->withHeader($claim, $this->prepareValue($value));
$builder = $builder->withHeader($claim, $this->prepareValue($value));
}
/** @noinspection PhpUnhandledExceptionInspection */
@@ -64,17 +103,17 @@ class Component extends BaseComponent {
* @param string $jwt
*
* @return Token
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function parse(string $jwt): Token {
return (new Parser())->parse($jwt);
return (new Parser(new JoseEncoder()))->parse($jwt);
}
public function verify(Token $token): bool {
try {
$algorithm = $this->getAlgorithmManager()->get($token->getHeader('alg'));
return $token->verify($algorithm->getSigner(), $algorithm->getPublicKey());
} catch (Exception $e) {
$algorithm = $this->getAlgorithmManager()->get($token->headers()->get('alg'));
return (new Validator())->validate($token, new SignedWith($algorithm->getSigner(), $algorithm->getPublicKey()));
} catch (Exception) {
return false;
}
}
@@ -92,8 +131,8 @@ class Component extends BaseComponent {
* @param string $encryptedValue
*
* @return string
* @throws \SodiumException
* @throws \RangeException
* @throws SodiumException
* @throws RangeException
*/
public function decryptValue(string $encryptedValue): string {
$decoded = Base64UrlSafe::decode($encryptedValue);
@@ -109,7 +148,7 @@ class Component extends BaseComponent {
}
public function getPublicKey(): string {
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->getContent();
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->contents();
}
private function getAlgorithmManager(): AlgorithmsManager {
@@ -122,9 +161,9 @@ class Component extends BaseComponent {
return $this->algorithmManager;
}
private function prepareValue($value) {
private function prepareValue(EncryptedValue|string $value): string {
if ($value instanceof EncryptedValue) {
return $this->encryptValue($value->getValue());
return $this->encryptValue($value->value);
}
return $value;

View File

@@ -3,19 +3,9 @@ declare(strict_types=1);
namespace api\components\Tokens;
class EncryptedValue {
final readonly class EncryptedValue {
/**
* @var string
*/
private $value;
public function __construct(string $value) {
$this->value = $value;
}
public function getValue(): string {
return $this->value;
public function __construct(public string $value) {
}
}

View File

@@ -3,19 +3,18 @@ declare(strict_types=1);
namespace api\components\Tokens;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Yii;
final class TokenReader {
final readonly class TokenReader {
private Token $token;
public function __construct(Token $token) {
$this->token = $token;
public function __construct(
private UnencryptedToken $token,
) {
}
public function getAccountId(): ?int {
$sub = $this->token->getClaim('sub', false);
$sub = $this->token->claims()->get('sub', false);
if ($sub === false) {
return null;
}
@@ -24,36 +23,36 @@ final class TokenReader {
return null;
}
return (int)mb_substr($sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
return (int)mb_substr((string)$sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
}
public function getClientId(): ?string {
return $this->token->getClaim('client_id', false) ?: null;
return $this->token->claims()->get('client_id', false) ?: null;
}
public function getScopes(): ?array {
$scopes = $this->token->getClaim('scope', false);
$scopes = $this->token->claims()->get('scope', false);
if ($scopes !== false) {
return explode(' ', $scopes);
return explode(' ', (string)$scopes);
}
// Handle legacy tokens, which used "ely-scopes" claim and was delimited with comma
$scopes = $this->token->getClaim('ely-scopes', false);
$scopes = $this->token->claims()->get('ely-scopes', false);
if ($scopes === false) {
return null;
}
return explode(',', $scopes);
return explode(',', (string)$scopes);
}
public function getMinecraftClientToken(): ?string {
$encodedClientToken = $this->token->getClaim('ely-client-token', false);
$encodedClientToken = $this->token->claims()->get('ely-client-token', false);
if ($encodedClientToken === false) {
return null;
}
/**
* It really might throw an exception but we have not seen any case of such exception yet
* It really might throw an exception, but we have not seen any case of such exception yet
* @noinspection PhpUnhandledExceptionInspection
*/
return Yii::$app->tokens->decryptValue($encodedClientToken);

View File

@@ -10,6 +10,7 @@ use common\models\Account;
use common\models\AccountSession;
use DateTime;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use Yii;
@@ -17,54 +18,52 @@ use yii\base\Component;
class TokensFactory extends Component {
public const SUB_ACCOUNT_PREFIX = 'ely|';
public const string SUB_ACCOUNT_PREFIX = 'ely|';
public function createForWebAccount(Account $account, AccountSession $session = null): Token {
public function createForWebAccount(Account $account, AccountSession $session = null): UnencryptedToken {
$payloads = [
'sub' => $this->buildSub($account->id),
'exp' => Carbon::now()->addHour()->getTimestamp(),
'exp' => Carbon::now()->addHour()->toDateTimeImmutable(),
'scope' => $this->prepareScopes([R::ACCOUNTS_WEB_USER]),
];
if ($session === null) {
// If we don't remember a session, the token should live longer
// so that the session doesn't end while working with the account
$payloads['exp'] = Carbon::now()->addDays(7)->getTimestamp();
$payloads['exp'] = Carbon::now()->addDays(7)->toDateTimeImmutable();
} else {
$payloads['jti'] = $session->id;
$payloads['jti'] = (string)$session->id;
}
return Yii::$app->tokens->create($payloads);
}
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): UnencryptedToken {
$payloads = [
'client_id' => $accessToken->getClient()->getIdentifier(),
'scope' => $this->prepareScopes($accessToken->getScopes()),
];
if ($accessToken->getExpiryDateTime() > new DateTime()) {
$payloads['exp'] = $accessToken->getExpiryDateTime()->getTimestamp();
$payloads['exp'] = $accessToken->getExpiryDateTime();
}
if ($accessToken->getUserIdentifier() !== null) {
$payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier());
$payloads['sub'] = $this->buildSub((int)$accessToken->getUserIdentifier());
}
return Yii::$app->tokens->create($payloads);
}
public function createForMinecraftAccount(Account $account, string $clientToken): Token {
public function createForMinecraftAccount(Account $account, string $clientToken): UnencryptedToken {
return Yii::$app->tokens->create([
'scope' => $this->prepareScopes([P::OBTAIN_OWN_ACCOUNT_INFO, P::MINECRAFT_SERVER_SESSION]),
'ely-client-token' => new EncryptedValue($clientToken),
'sub' => $this->buildSub($account->id),
'exp' => Carbon::now()->addDays(2)->getTimestamp(),
'exp' => Carbon::now()->addDays(2)->toDateTimeImmutable(),
]);
}
/**
* @param ScopeEntityInterface[]|string[] $scopes
*
* @return string
*/
private function prepareScopes(array $scopes): string {
return implode(' ', array_map(function($scope): string {