PHPStan fixes.

This commit is contained in:
flash 2025-02-26 22:03:13 +00:00
parent 446c675613
commit 17251cf750
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
19 changed files with 326 additions and 66 deletions

11
composer.lock generated
View file

@ -1041,16 +1041,16 @@
},
{
"name": "matomo/device-detector",
"version": "6.4.3",
"version": "6.4.5",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
"reference": "aa4586d495a7f59029d46d976f160b13eb769bb0"
"reference": "270bbc41f80994e80805ac377b67324eba53c412"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/aa4586d495a7f59029d46d976f160b13eb769bb0",
"reference": "aa4586d495a7f59029d46d976f160b13eb769bb0",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/270bbc41f80994e80805ac377b67324eba53c412",
"reference": "270bbc41f80994e80805ac377b67324eba53c412",
"shasum": ""
},
"require": {
@ -1067,6 +1067,7 @@
"phpunit/phpunit": "^8.5.8",
"psr/cache": "^1.0.1",
"psr/simple-cache": "^1.0.1",
"slevomat/coding-standard": "<8.16.0",
"symfony/yaml": "^5.1.7"
},
"suggest": {
@ -1106,7 +1107,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
"time": "2025-01-17T09:59:39+00:00"
"time": "2025-02-26T17:37:32+00:00"
},
{
"name": "mustangostang/spyc",

View file

@ -12,6 +12,9 @@ class ScopesData {
$this->cache = new DbStatementCache($dbConn);
}
/**
* @return iterable<ScopeInfo>
*/
public function getScopes(
?bool $restricted = null,
?bool $deprecated = null

View file

@ -8,7 +8,6 @@ use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\CommentsContext;
use Misuzu\Users\UsersContext;
@ -21,7 +20,6 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
private ChangelogData $changelog,
private UsersContext $usersCtx,
private CommentsContext $commentsCtx,
private AuthInfo $authInfo,
) {}
#[HttpGet('/changelog')]

View file

@ -27,6 +27,15 @@ class CommentsRoutes implements RouteHandler, UrlSource {
: new PermissionResult(0);
}
/**
* @return array{
* id: string,
* name: string,
* profile: string,
* avatar: string,
* colour?: string,
* }
*/
private function convertUser(UserInfo $userInfo, int $avatarRes = 80): array {
$user = [
'id' => $userInfo->id,
@ -42,6 +51,10 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $user;
}
/**
* @param iterable<CommentsPostInfo> $postInfos
* @return mixed[]
*/
private function convertPosts(
IPermissionResult $perms,
CommentsCategoryInfo $catInfo,
@ -66,6 +79,31 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $posts;
}
/**
* @param ?iterable<CommentsPostInfo> $replyInfos
* @return array{
* id: string,
* body?: string,
* created: string,
* pinned?: string,
* edited?: string,
* deleted?: string|true,
* user?: array{
* id: string,
* name: string,
* profile: string,
* avatar: string,
* colour?: string,
* },
* positive?: int,
* negative?: int,
* vote?: int,
* can_edit?: true,
* can_delete?: true,
* can_delete_any?: true,
* replies?: int|mixed[],
* }
*/
private function convertPost(
IPermissionResult $perms,
CommentsCategoryInfo $catInfo,
@ -86,7 +124,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
if($postInfo->edited)
$post['edited'] = $postInfo->editedAt->toIso8601ZuluString();
if(!$isDeleted && $postInfo->userId !== null)
if($postInfo->userId !== null)
try {
$post['user'] = $this->convertUser(
$this->usersCtx->getUserInfo($postInfo->userId)
@ -132,6 +170,10 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $post;
}
/**
* @param array<string, mixed> $extra
* @return array{error: array{name: string, text: string}}
*/
private static function error(HttpResponseBuilder $response, int $code, string $name, string $text, array $extra = []): array {
$response->statusCode = $code;
@ -143,7 +185,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
];
}
/** @return void|int|array{error: array{name: string, text: string}} */
/** @return void|array{error: array{name: string, text: string}} */
#[HttpMiddleware('/comments')]
public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) {
if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) {
@ -158,6 +200,34 @@ class CommentsRoutes implements RouteHandler, UrlSource {
$response->setHeader('X-CSRF-Token', CSRF::token());
}
/**
* @return array{
* category: array{
* name: string,
* created: string,
* locked?: string,
* owner?: array{
* id: string,
* name: string,
* profile: string,
* avatar: string,
* colour?: string,
* },
* },
* user?: array{
* id: string,
* name: string,
* profile: string,
* avatar: string,
* colour?: string,
* can_create?: true,
* can_pin?: true,
* can_vote?: true,
* can_lock?: true,
* },
* posts: mixed[]
* }|array{error: array{name: string, text: string}}
*/
#[HttpGet('/comments/categories/([A-Za-z0-9-]+)')]
public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array {
try {
@ -214,6 +284,12 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $result;
}
/**
* @return array{
* name: string,
* locked?: string|false,
* }|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/categories/([A-Za-z0-9-]+)')]
public function patchCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array {
if(!($request->content instanceof FormHttpContent))
@ -253,6 +329,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $result;
}
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts')]
public function postPost(HttpResponseBuilder $response, HttpRequest $request): array {
if(!($request->content instanceof FormHttpContent))
@ -314,6 +393,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $this->convertPost($perms, $catInfo, $postInfo);
}
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpGet('/comments/posts/([0-9]+)')]
public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
try {
@ -336,6 +418,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $post;
}
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpGet('/comments/posts/([0-9]+)/replies')]
public function getPostReplies(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
try {
@ -352,9 +437,17 @@ class CommentsRoutes implements RouteHandler, UrlSource {
);
}
/**
* @return array{
* id: string,
* body?: string,
* pinned?: string|false,
* edited?: string,
* }|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)')]
// this should be HttpPatch but PHP doesn't parse into $_POST for PATCH...
// fix this in the v3 router for index by just ignoring PHP's parsing altogether
#[HttpPost('/comments/posts/([0-9]+)')]
public function patchPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!($request->content instanceof FormHttpContent))
return self::error($response, 400, 'comments:content', 'Provided content could not be understood.');
@ -422,6 +515,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $result;
}
/**
* @return string|array{error: array{name: string, text: string}}
*/
#[HttpDelete('/comments/posts/([0-9]+)')]
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array|string {
try {
@ -448,6 +544,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return '';
}
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/restore')]
public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
@ -470,6 +569,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return [];
}
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/nuke')]
public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
@ -492,6 +594,13 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return [];
}
/**
* @return array{
* vote: int,
* positive: int,
* negative: int,
* }|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/vote')]
public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!($request->content instanceof FormHttpContent))
@ -533,6 +642,13 @@ class CommentsRoutes implements RouteHandler, UrlSource {
];
}
/**
* @return array{
* vote: int,
* positive: int,
* negative: int,
* }|array{error: array{name: string, text: string}}
*/
#[HttpDelete('/comments/posts/([0-9]+)/vote')]
public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_VOTE))

View file

@ -71,7 +71,7 @@ class ForumPostsData {
/**
* @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo
* @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery
* @param ?array{type?: string, author?: string, after?: string, query_string?: string, topic?: string} $searchQuery
* @return \Iterator<int, ForumPostInfo>|ForumPostInfo[]
*/
public function getPosts(

View file

@ -15,7 +15,7 @@ class ArrayJWKSet implements JWKSet {
return $this->keys[array_key_first($this->keys)];
foreach($this->keys as $key)
if($algo !== null && $key->algo !== null && hash_equals($key->algo, $algo))
if($key->algo !== null && hash_equals($key->algo, $algo))
return $key;
throw new RuntimeException('could not find a key that matched the requested algorithm');
@ -31,6 +31,9 @@ class ArrayJWKSet implements JWKSet {
return $key;
}
/**
* @param string|mixed[]|object $json
*/
public static function decodeJson(string|array|object $json): ArrayJWKSet {
if(is_object($json))
$json = (array)$json;

View file

@ -8,15 +8,19 @@ use Index\{UriBase64,XDateTime};
// based on https://github.com/firebase/php-jwt/blob/8f718f4dfc9c5d5f0c994cdfd103921b43592712/src/JWT.php
class JWT {
/**
* @param array<string, mixed>|object $payload
* @param array<string, mixed>|object|null $headers
*/
public static function encode(
array|object $payload,
JWK $key,
?string $alg = null,
array|object|null $header = null
array|object|null $headers = null
): string {
$head = ['typ' => 'JWT'];
if($header !== null)
$head = array_merge($head, (array)$header);
if($headers !== null)
$head = array_merge($head, (array)$headers);
$head['alg'] = $alg ?? $key->algo;
if($head['alg'] === null)
throw new InvalidArgumentException('$key does not provide a default algorithm, $alg must be specified');
@ -29,6 +33,10 @@ class JWT {
return $encoded;
}
/**
* @param array<string, mixed>|object|null $headers
* @return array<string, mixed>|object
*/
public static function decode(
string $token,
JWKSet $keys,

View file

@ -1,6 +1,7 @@
<?php
namespace Misuzu\JWT;
use InvalidArgumentException;
use RuntimeException;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey};
@ -35,10 +36,11 @@ class SecLibJWK implements JWK {
}
public function sign(string $data, SecLibJWKAlgo|string|null $algo = null): string {
if(!($this->key instanceof PrivateKey))
$key = $this->resolveAlgo($algo)->transform($this->key);
if(!($key instanceof PrivateKey))
throw new RuntimeException('this instance does not have a private key, it can only be used to verify');
return $this->resolveAlgo($algo)->transform($this->key)->sign($data);
return $key->sign($data);
}
public function verify(string $data, string $signature, SecLibJWKAlgo|string|null $algo = null): bool {

View file

@ -29,25 +29,29 @@ class SecLibJWKAlgo {
return new SecLibJWKAlgo($algo, self::$algos[$algo]);
}
public static function ensureRSAKey(AsymmetricKey $key): void {
if(!($key instanceof RSA))
public static function ensureRSAKey(AsymmetricKey $key): RSA {
if($key instanceof RSA)
return $key;
throw new InvalidArgumentException('$key must be an RSA key');
}
public static function applyPKCS1Padding(AsymmetricKey $key): AsymmetricKey {
public static function applyPKCS1Padding(RSA $key): RSA {
return $key->withPadding(RSA::SIGNATURE_PKCS1);
}
public static function applyPSSPadding(AsymmetricKey $key): AsymmetricKey {
public static function applyPSSPadding(RSA $key): RSA {
return $key->withPadding(RSA::SIGNATURE_PSS);
}
public static function ensureECKey(AsymmetricKey $key): void {
if(!($key instanceof EC))
public static function ensureECKey(AsymmetricKey $key): EC {
if($key instanceof EC)
return $key;
throw new InvalidArgumentException('$key must be an EC key');
}
public static function applyIEEESignatureFormat(AsymmetricKey $key): AsymmetricKey {
public static function applyIEEESignatureFormat(EC $key): EC {
return $key->withSignatureFormat('IEEE');
}
@ -56,70 +60,70 @@ class SecLibJWKAlgo {
return array_keys(self::$algos);
}
public static function transformRS256(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformRS256(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha256');
}
public static function transformRS384(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformRS384(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha384');
}
public static function transformRS512(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformRS512(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha512');
}
public static function transformPS256(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformPS256(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformPS384(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformPS384(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformPS512(AsymmetricKey $key): AsymmetricKey {
self::ensureRSAKey($key);
public static function transformPS512(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformES256(AsymmetricKey $key): AsymmetricKey {
self::ensureECKey($key);
public static function transformES256(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256r1')
throw new InvalidArgumentException('curve must be secp256r1');
return self::applyIEEESignatureFormat($key)->withHash('sha256');
}
public static function transformES256K(AsymmetricKey $key): AsymmetricKey {
self::ensureECKey($key);
public static function transformES256K(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256k1')
throw new InvalidArgumentException('curve must be secp256k1');
return self::applyIEEESignatureFormat($key)->withHash('sha256');
}
public static function transformES384(AsymmetricKey $key): AsymmetricKey {
self::ensureECKey($key);
public static function transformES384(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp384r1')
throw new InvalidArgumentException('curve must be secp384r1');
return self::applyIEEESignatureFormat($key)->withHash('sha384');
}
public static function transformES512(AsymmetricKey $key): AsymmetricKey {
self::ensureECKey($key);
public static function transformES512(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp521r1')
throw new InvalidArgumentException('curve must be secp521r1');
return self::applyIEEESignatureFormat($key)->withHash('sha512');
}
public static function transformEdDSA(AsymmetricKey $key): AsymmetricKey {
self::ensureECKey($key);
public static function transformEdDSA(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'Ed25519' && $key->getCurve() !== 'Ed448')
throw new InvalidArgumentException('curve must be Ed25519 or Ed448');

View file

@ -8,7 +8,6 @@ use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\CommentsContext;
use Misuzu\Parsers\{Parsers,TextFormat};
use Misuzu\Users\{UsersContext,UserInfo};
@ -18,7 +17,6 @@ class NewsRoutes implements RouteHandler, UrlSource {
public function __construct(
private SiteInfo $siteInfo,
private AuthInfo $authInfo,
private UrlRegistry $urls,
private UsersContext $usersCtx,
private CommentsContext $commentsCtx,

View file

@ -3,6 +3,7 @@ namespace Misuzu\OAuth2;
use RuntimeException;
use Index\XArray;
use Index\Colour\{Colour,ColourRgb};
use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
@ -48,6 +49,14 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
return $result;
}
/**
* @return int|array{
* resource: string,
* authorization_servers: string[],
* scopes_supported: string[],
* bearer_methods_supported: string[],
* }
*/
#[HttpOptions('/.well-known/oauth-protected-resource')]
#[HttpGet('/.well-known/oauth-protected-resource')]
public function getWellKnownProtectedResource(HttpResponseBuilder $response, HttpRequest $request): array|int {
@ -63,6 +72,24 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
];
}
/**
* @return int|array{
* issuer: string,
* authorization_endpoint: string,
* token_endpoint: string,
* jwks_uri: string,
* scopes_supported: string[],
* response_types_supported: string[],
* response_modes_supported: string[],
* grant_types_supported: string[],
* token_endpoint_auth_methods_supported: string[],
* revocation_endpoint?: string,
* revocation_endpoint_auth_methods_supported?: string[],
* introspection_endpoint?: string,
* introspection_endpoint_auth_methods_supported?: string[],
* code_challenge_methods_supported: string[],
* }
*/
#[HttpOptions('/.well-known/oauth-authorization-server')]
#[HttpGet('/.well-known/oauth-authorization-server')]
public function getWellKnownAuthorizationServer(HttpResponseBuilder $response, HttpRequest $request): array|int {
@ -104,6 +131,21 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
];
}
/**
* @return int|array{
* issuer: string,
* authorization_endpoint: string,
* token_endpoint: string,
* userinfo_endpoint: string,
* jwks_uri: string,
* response_types_supported: string[],
* response_modes_supported: string[],
* grant_types_supported: string[],
* subject_types_supported: string[],
* id_token_signing_alg_values_supported: string[],
* token_endpoint_auth_methods_supported: string[],
* }
*/
#[HttpOptions('/.well-known/openid-configuration')]
#[HttpGet('/.well-known/openid-configuration')]
public function getWellKnown(HttpResponseBuilder $response, HttpRequest $request): array|int {
@ -125,13 +167,19 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
'jwks_uri' => sprintf('%s%s', $this->siteInfo->url, $this->urls->format('oauth2-jwks')),
'response_types_supported' => ['code', 'code id_token'],
'response_modes_supported' => ['query'],
'grant_types_supported' => ['authorization_code'],
'grant_types_supported' => [
'authorization_code',
//'client_credentials', <-- supported but makes NO sense for openid
'refresh_token',
'urn:ietf:params:oauth:grant-type:device_code',
],
'subject_types_supported' => ['public'],
'id_token_signing_alg_values_supported' => $signingAlgs,
'token_endpoint_auth_methods_supported' => ['client_secret_basic', 'client_secret_post'],
'token_endpoint_auth_methods_supported' => ['none', 'client_secret_basic', 'client_secret_post'],
];
}
/** @return int|array{keys?: array<string, string>} */
#[HttpOptions('/oauth2/jwks.json')]
#[HttpOptions('/openid/jwks.json')]
#[HttpGet('/oauth2/jwks.json')]
@ -207,7 +255,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
}
/**
* @return array{
* @return int|array{
* access_token: string,
* token_type: 'Bearer',
* expires_in?: int,
@ -324,6 +372,28 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
]);
}
/**
* @return int|array{
* sub: string,
* name?: string,
* nickname?: string,
* preferred_username?: string,
* profile?: string,
* picture?: string,
* zoneinfo?: string,
* birthdate?: string,
* website?: string,
* 'http://railgun.sh/country_code'?: string,
* 'http://railgun.sh/colour_raw'?: int,
* 'http://railgun.sh/colour_css'?: string,
* 'http://railgun.sh/rank'?: int,
* 'http://railgun.sh/banned'?: bool,
* 'http://railgun.sh/roles'?: string[],
* 'http://railgun.sh/is_super'?: bool,
* email?: string,
* email_verified?: bool,
* }|array{ error: string, error_description: string }
*/
#[HttpOptions('/oauth2/userinfo')]
#[HttpOptions('/openid/userinfo')]
#[HttpGet('/oauth2/userinfo')]

View file

@ -80,6 +80,17 @@ class OAuth2Context {
);
}
/**
* @return array{
* iss: string,
* sub: string,
* aud: string,
* exp: int,
* iat: int,
* auth_time: int,
* nonce?: string,
* }
*/
public static function getDataForIdToken(
SiteInfo $siteInfo,
AppInfo $appInfo,

View file

@ -38,7 +38,11 @@ class OAuth2Keys {
if($body === false)
throw new RuntimeException(sprintf('public key "%s" could not be read', $keyId));
return PublicKeyLoader::loadPublicKey($body);
$key = PublicKeyLoader::loadPublicKey($body);
if(!($key instanceof AsymmetricKey))
throw new RuntimeException(sprintf('public key "%s" could not be loaded', $keyId));
return $key;
}
public function getPrivateKey(string $keyId): AsymmetricKey&PrivateKey {
@ -50,7 +54,11 @@ class OAuth2Keys {
if($body === false)
throw new RuntimeException(sprintf('private key "%s" could not be read', $keyId));
return PublicKeyLoader::loadPrivateKey($body);
$key = PublicKeyLoader::loadPrivateKey($body);
if(!($key instanceof AsymmetricKey))
throw new RuntimeException(sprintf('private key "%s" could not be loaded', $keyId));
return $key;
}
public function getPublicKeySet(): JWKSet {
@ -77,19 +85,23 @@ class OAuth2Keys {
return new ArrayJWKSet($keys);
}
/** @return array<string, mixed[]> */
/** @return array{keys?: array<string, string>} */
public function getPublicKeysForJson(): array {
$keys = [];
foreach($this->keys as $keyInfo)
$keys = array_merge_recursive(
$keys,
json_decode($this->getPublicKey($keyInfo->id)->toString('JWK', [
foreach($this->keys as $keyInfo) {
$options = [
'kid' => $keyInfo->id,
'use' => 'sig',
'alg' => $keyInfo->alg,
]), true)
];
if($keyInfo->alg !== null)
$options['alg'] = $keyInfo->alg;
$keys = array_merge_recursive(
$keys,
json_decode($this->getPublicKey($keyInfo->id)->toString('JWK', $options), true)
);
}
return $keys;
}

View file

@ -33,7 +33,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
/**
* @return int|array{
* error: 'auth'|'csrf'|'method'|'length'|'client'|'scope'|'format'|'required'|'authorise',
* error: 'auth'|'csrf'|'method'|'length'|'client'|'scope'|'format'|'required'|'authorise'|'resptype',
* scope?: string,
* reason?: string,
* }|array{

View file

@ -5,6 +5,10 @@ use InvalidArgumentException;
use Index\XArray;
class BasicWebFingerLink implements WebFingerLink {
/**
* @param WebFingerTitle[] $titles
* @param WebFingerProperty[] $properties
*/
public function __construct(
public private(set) string $relation,
public private(set) ?string $type = null,

View file

@ -4,6 +4,11 @@ namespace Misuzu\WebFinger;
use InvalidArgumentException;
class BasicWebFingerResourceInfo implements WebFingerResourceInfo {
/**
* @param string[] $aliases
* @param WebFingerProperty[] $properties
* @param WebFingerLink[] $links
*/
public function __construct(
public private(set) ?string $subject = null,
public private(set) array $aliases = [],

View file

@ -10,7 +10,7 @@ class WebFingerRegistry {
$this->resolvers[] = $resolver;
}
/** @return WebFingerResolver[] */
/** @return WebFingerResourceInfo[] */
public function resolve(string $uri): array {
$results = [];

View file

@ -2,5 +2,6 @@
namespace Misuzu\WebFinger;
interface WebFingerResolver {
/** @param array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path: string, query?: string, fragment?: string} $components */
public function resolve(string $uri, array $components): ?WebFingerResourceInfo;
}

View file

@ -12,7 +12,10 @@ class WebFingerRoutes implements RouteHandler {
private WebFingerRegistry $registry
) {}
/** @param WebFingerProperty[] $propInfos */
/**
* @param WebFingerProperty[] $propInfos
* @return array<string, ?string>
*/
private static function extractProps(array $propInfos): array {
$props = [];
foreach($propInfos as $propInfo)
@ -20,7 +23,10 @@ class WebFingerRoutes implements RouteHandler {
return $props;
}
/** @param WebFingerTitle[] $titleInfos */
/**
* @param WebFingerTitle[] $titleInfos
* @return array<string, string>
*/
private static function extractTitles(array $titleInfos): array {
$titles = [];
foreach($titleInfos as $titleInfo)
@ -28,6 +34,10 @@ class WebFingerRoutes implements RouteHandler {
return $titles;
}
/**
* @param WebFingerLink[] $linkInfos
* @return array{rel: string, type?: string, href?: string, titles?: array<string, string>, properties?: array<string, ?string>}[]
*/
private static function extractLinks(array $linkInfos): array {
$links = [];
foreach($linkInfos as $linkInfo) {
@ -45,6 +55,20 @@ class WebFingerRoutes implements RouteHandler {
return $links;
}
/**
* @return int|array{
* subject?: string,
* aliases?: string[],
* properties?: array<string, ?string>,
* links?: array{
* rel: string,
* type?: string,
* href?: string,
* titles?: array<string, string>,
* properties?: array<string, ?string>
* }[]
* }
*/
#[HttpOptions('/.well-known/webfinger')]
#[HttpGet('/.well-known/webfinger')]
public function getWebFinger(HttpResponseBuilder $response, HttpRequest $request): array|int {