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

View file

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

View file

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

View file

@ -27,6 +27,15 @@ class CommentsRoutes implements RouteHandler, UrlSource {
: new PermissionResult(0); : new PermissionResult(0);
} }
/**
* @return array{
* id: string,
* name: string,
* profile: string,
* avatar: string,
* colour?: string,
* }
*/
private function convertUser(UserInfo $userInfo, int $avatarRes = 80): array { private function convertUser(UserInfo $userInfo, int $avatarRes = 80): array {
$user = [ $user = [
'id' => $userInfo->id, 'id' => $userInfo->id,
@ -42,6 +51,10 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $user; return $user;
} }
/**
* @param iterable<CommentsPostInfo> $postInfos
* @return mixed[]
*/
private function convertPosts( private function convertPosts(
IPermissionResult $perms, IPermissionResult $perms,
CommentsCategoryInfo $catInfo, CommentsCategoryInfo $catInfo,
@ -66,6 +79,31 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $posts; 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( private function convertPost(
IPermissionResult $perms, IPermissionResult $perms,
CommentsCategoryInfo $catInfo, CommentsCategoryInfo $catInfo,
@ -86,7 +124,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
if($postInfo->edited) if($postInfo->edited)
$post['edited'] = $postInfo->editedAt->toIso8601ZuluString(); $post['edited'] = $postInfo->editedAt->toIso8601ZuluString();
if(!$isDeleted && $postInfo->userId !== null) if($postInfo->userId !== null)
try { try {
$post['user'] = $this->convertUser( $post['user'] = $this->convertUser(
$this->usersCtx->getUserInfo($postInfo->userId) $this->usersCtx->getUserInfo($postInfo->userId)
@ -132,6 +170,10 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $post; 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 { private static function error(HttpResponseBuilder $response, int $code, string $name, string $text, array $extra = []): array {
$response->statusCode = $code; $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')] #[HttpMiddleware('/comments')]
public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) { public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) {
if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) { if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) {
@ -158,6 +200,34 @@ class CommentsRoutes implements RouteHandler, UrlSource {
$response->setHeader('X-CSRF-Token', CSRF::token()); $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-]+)')] #[HttpGet('/comments/categories/([A-Za-z0-9-]+)')]
public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array { public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array {
try { try {
@ -214,6 +284,12 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $result; return $result;
} }
/**
* @return array{
* name: string,
* locked?: string|false,
* }|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/categories/([A-Za-z0-9-]+)')] #[HttpPost('/comments/categories/([A-Za-z0-9-]+)')]
public function patchCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array { public function patchCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array {
if(!($request->content instanceof FormHttpContent)) if(!($request->content instanceof FormHttpContent))
@ -253,6 +329,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $result; return $result;
} }
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts')] #[HttpPost('/comments/posts')]
public function postPost(HttpResponseBuilder $response, HttpRequest $request): array { public function postPost(HttpResponseBuilder $response, HttpRequest $request): array {
if(!($request->content instanceof FormHttpContent)) if(!($request->content instanceof FormHttpContent))
@ -314,6 +393,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $this->convertPost($perms, $catInfo, $postInfo); return $this->convertPost($perms, $catInfo, $postInfo);
} }
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpGet('/comments/posts/([0-9]+)')] #[HttpGet('/comments/posts/([0-9]+)')]
public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
try { try {
@ -336,6 +418,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return $post; return $post;
} }
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpGet('/comments/posts/([0-9]+)/replies')] #[HttpGet('/comments/posts/([0-9]+)/replies')]
public function getPostReplies(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function getPostReplies(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
try { 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... // 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 // 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 { public function patchPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!($request->content instanceof FormHttpContent)) if(!($request->content instanceof FormHttpContent))
return self::error($response, 400, 'comments:content', 'Provided content could not be understood.'); 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 $result;
} }
/**
* @return string|array{error: array{name: string, text: string}}
*/
#[HttpDelete('/comments/posts/([0-9]+)')] #[HttpDelete('/comments/posts/([0-9]+)')]
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array|string { public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array|string {
try { try {
@ -448,6 +544,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return ''; return '';
} }
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/restore')] #[HttpPost('/comments/posts/([0-9]+)/restore')]
public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY)) if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
@ -470,6 +569,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return []; return [];
} }
/**
* @return mixed[]|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/nuke')] #[HttpPost('/comments/posts/([0-9]+)/nuke')]
public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY)) if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
@ -492,6 +594,13 @@ class CommentsRoutes implements RouteHandler, UrlSource {
return []; return [];
} }
/**
* @return array{
* vote: int,
* positive: int,
* negative: int,
* }|array{error: array{name: string, text: string}}
*/
#[HttpPost('/comments/posts/([0-9]+)/vote')] #[HttpPost('/comments/posts/([0-9]+)/vote')]
public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!($request->content instanceof FormHttpContent)) 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')] #[HttpDelete('/comments/posts/([0-9]+)/vote')]
public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_VOTE)) 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 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[] * @return \Iterator<int, ForumPostInfo>|ForumPostInfo[]
*/ */
public function getPosts( public function getPosts(

View file

@ -15,7 +15,7 @@ class ArrayJWKSet implements JWKSet {
return $this->keys[array_key_first($this->keys)]; return $this->keys[array_key_first($this->keys)];
foreach($this->keys as $key) 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; return $key;
throw new RuntimeException('could not find a key that matched the requested algorithm'); throw new RuntimeException('could not find a key that matched the requested algorithm');
@ -31,6 +31,9 @@ class ArrayJWKSet implements JWKSet {
return $key; return $key;
} }
/**
* @param string|mixed[]|object $json
*/
public static function decodeJson(string|array|object $json): ArrayJWKSet { public static function decodeJson(string|array|object $json): ArrayJWKSet {
if(is_object($json)) if(is_object($json))
$json = (array)$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 // based on https://github.com/firebase/php-jwt/blob/8f718f4dfc9c5d5f0c994cdfd103921b43592712/src/JWT.php
class JWT { class JWT {
/**
* @param array<string, mixed>|object $payload
* @param array<string, mixed>|object|null $headers
*/
public static function encode( public static function encode(
array|object $payload, array|object $payload,
JWK $key, JWK $key,
?string $alg = null, ?string $alg = null,
array|object|null $header = null array|object|null $headers = null
): string { ): string {
$head = ['typ' => 'JWT']; $head = ['typ' => 'JWT'];
if($header !== null) if($headers !== null)
$head = array_merge($head, (array)$header); $head = array_merge($head, (array)$headers);
$head['alg'] = $alg ?? $key->algo; $head['alg'] = $alg ?? $key->algo;
if($head['alg'] === null) if($head['alg'] === null)
throw new InvalidArgumentException('$key does not provide a default algorithm, $alg must be specified'); throw new InvalidArgumentException('$key does not provide a default algorithm, $alg must be specified');
@ -29,6 +33,10 @@ class JWT {
return $encoded; return $encoded;
} }
/**
* @param array<string, mixed>|object|null $headers
* @return array<string, mixed>|object
*/
public static function decode( public static function decode(
string $token, string $token,
JWKSet $keys, JWKSet $keys,

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Misuzu\JWT; namespace Misuzu\JWT;
use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey}; 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 { 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'); 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 { 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]); return new SecLibJWKAlgo($algo, self::$algos[$algo]);
} }
public static function ensureRSAKey(AsymmetricKey $key): void { public static function ensureRSAKey(AsymmetricKey $key): RSA {
if(!($key instanceof RSA)) if($key instanceof RSA)
return $key;
throw new InvalidArgumentException('$key must be an RSA 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); 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); return $key->withPadding(RSA::SIGNATURE_PSS);
} }
public static function ensureECKey(AsymmetricKey $key): void { public static function ensureECKey(AsymmetricKey $key): EC {
if(!($key instanceof EC)) if($key instanceof EC)
return $key;
throw new InvalidArgumentException('$key must be an EC 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'); return $key->withSignatureFormat('IEEE');
} }
@ -56,70 +60,70 @@ class SecLibJWKAlgo {
return array_keys(self::$algos); return array_keys(self::$algos);
} }
public static function transformRS256(AsymmetricKey $key): AsymmetricKey { public static function transformRS256(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha256'); return self::applyPKCS1Padding($key)->withHash('sha256');
} }
public static function transformRS384(AsymmetricKey $key): AsymmetricKey { public static function transformRS384(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha384'); return self::applyPKCS1Padding($key)->withHash('sha384');
} }
public static function transformRS512(AsymmetricKey $key): AsymmetricKey { public static function transformRS512(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha512'); return self::applyPKCS1Padding($key)->withHash('sha512');
} }
public static function transformPS256(AsymmetricKey $key): AsymmetricKey { public static function transformPS256(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512'); return self::applyPSSPadding($key)->withHash('sha512');
} }
public static function transformPS384(AsymmetricKey $key): AsymmetricKey { public static function transformPS384(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512'); return self::applyPSSPadding($key)->withHash('sha512');
} }
public static function transformPS512(AsymmetricKey $key): AsymmetricKey { public static function transformPS512(AsymmetricKey $key): RSA {
self::ensureRSAKey($key); $key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512'); return self::applyPSSPadding($key)->withHash('sha512');
} }
public static function transformES256(AsymmetricKey $key): AsymmetricKey { public static function transformES256(AsymmetricKey $key): EC {
self::ensureECKey($key); $key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256r1') if($key->getCurve() !== 'secp256r1')
throw new InvalidArgumentException('curve must be secp256r1'); throw new InvalidArgumentException('curve must be secp256r1');
return self::applyIEEESignatureFormat($key)->withHash('sha256'); return self::applyIEEESignatureFormat($key)->withHash('sha256');
} }
public static function transformES256K(AsymmetricKey $key): AsymmetricKey { public static function transformES256K(AsymmetricKey $key): EC {
self::ensureECKey($key); $key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256k1') if($key->getCurve() !== 'secp256k1')
throw new InvalidArgumentException('curve must be secp256k1'); throw new InvalidArgumentException('curve must be secp256k1');
return self::applyIEEESignatureFormat($key)->withHash('sha256'); return self::applyIEEESignatureFormat($key)->withHash('sha256');
} }
public static function transformES384(AsymmetricKey $key): AsymmetricKey { public static function transformES384(AsymmetricKey $key): EC {
self::ensureECKey($key); $key = self::ensureECKey($key);
if($key->getCurve() !== 'secp384r1') if($key->getCurve() !== 'secp384r1')
throw new InvalidArgumentException('curve must be secp384r1'); throw new InvalidArgumentException('curve must be secp384r1');
return self::applyIEEESignatureFormat($key)->withHash('sha384'); return self::applyIEEESignatureFormat($key)->withHash('sha384');
} }
public static function transformES512(AsymmetricKey $key): AsymmetricKey { public static function transformES512(AsymmetricKey $key): EC {
self::ensureECKey($key); $key = self::ensureECKey($key);
if($key->getCurve() !== 'secp521r1') if($key->getCurve() !== 'secp521r1')
throw new InvalidArgumentException('curve must be secp521r1'); throw new InvalidArgumentException('curve must be secp521r1');
return self::applyIEEESignatureFormat($key)->withHash('sha512'); return self::applyIEEESignatureFormat($key)->withHash('sha512');
} }
public static function transformEdDSA(AsymmetricKey $key): AsymmetricKey { public static function transformEdDSA(AsymmetricKey $key): EC {
self::ensureECKey($key); $key = self::ensureECKey($key);
if($key->getCurve() !== 'Ed25519' && $key->getCurve() !== 'Ed448') if($key->getCurve() !== 'Ed25519' && $key->getCurve() !== 'Ed448')
throw new InvalidArgumentException('curve must be Ed25519 or 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\Syndication\FeedBuilder;
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
use Misuzu\{Pagination,SiteInfo,Template}; use Misuzu\{Pagination,SiteInfo,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\CommentsContext; use Misuzu\Comments\CommentsContext;
use Misuzu\Parsers\{Parsers,TextFormat}; use Misuzu\Parsers\{Parsers,TextFormat};
use Misuzu\Users\{UsersContext,UserInfo}; use Misuzu\Users\{UsersContext,UserInfo};
@ -18,7 +17,6 @@ class NewsRoutes implements RouteHandler, UrlSource {
public function __construct( public function __construct(
private SiteInfo $siteInfo, private SiteInfo $siteInfo,
private AuthInfo $authInfo,
private UrlRegistry $urls, private UrlRegistry $urls,
private UsersContext $usersCtx, private UsersContext $usersCtx,
private CommentsContext $commentsCtx, private CommentsContext $commentsCtx,

View file

@ -3,6 +3,7 @@ namespace Misuzu\OAuth2;
use RuntimeException; use RuntimeException;
use Index\XArray; use Index\XArray;
use Index\Colour\{Colour,ColourRgb};
use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest}; use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon}; use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon};
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
@ -48,6 +49,14 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
return $result; return $result;
} }
/**
* @return int|array{
* resource: string,
* authorization_servers: string[],
* scopes_supported: string[],
* bearer_methods_supported: string[],
* }
*/
#[HttpOptions('/.well-known/oauth-protected-resource')] #[HttpOptions('/.well-known/oauth-protected-resource')]
#[HttpGet('/.well-known/oauth-protected-resource')] #[HttpGet('/.well-known/oauth-protected-resource')]
public function getWellKnownProtectedResource(HttpResponseBuilder $response, HttpRequest $request): array|int { 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')] #[HttpOptions('/.well-known/oauth-authorization-server')]
#[HttpGet('/.well-known/oauth-authorization-server')] #[HttpGet('/.well-known/oauth-authorization-server')]
public function getWellKnownAuthorizationServer(HttpResponseBuilder $response, HttpRequest $request): array|int { 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')] #[HttpOptions('/.well-known/openid-configuration')]
#[HttpGet('/.well-known/openid-configuration')] #[HttpGet('/.well-known/openid-configuration')]
public function getWellKnown(HttpResponseBuilder $response, HttpRequest $request): array|int { 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')), 'jwks_uri' => sprintf('%s%s', $this->siteInfo->url, $this->urls->format('oauth2-jwks')),
'response_types_supported' => ['code', 'code id_token'], 'response_types_supported' => ['code', 'code id_token'],
'response_modes_supported' => ['query'], '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'], 'subject_types_supported' => ['public'],
'id_token_signing_alg_values_supported' => $signingAlgs, '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('/oauth2/jwks.json')]
#[HttpOptions('/openid/jwks.json')] #[HttpOptions('/openid/jwks.json')]
#[HttpGet('/oauth2/jwks.json')] #[HttpGet('/oauth2/jwks.json')]
@ -207,7 +255,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
} }
/** /**
* @return array{ * @return int|array{
* access_token: string, * access_token: string,
* token_type: 'Bearer', * token_type: 'Bearer',
* expires_in?: int, * 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('/oauth2/userinfo')]
#[HttpOptions('/openid/userinfo')] #[HttpOptions('/openid/userinfo')]
#[HttpGet('/oauth2/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( public static function getDataForIdToken(
SiteInfo $siteInfo, SiteInfo $siteInfo,
AppInfo $appInfo, AppInfo $appInfo,

View file

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

View file

@ -33,7 +33,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
/** /**
* @return int|array{ * @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, * scope?: string,
* reason?: string, * reason?: string,
* }|array{ * }|array{

View file

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

View file

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

View file

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

View file

@ -2,5 +2,6 @@
namespace Misuzu\WebFinger; namespace Misuzu\WebFinger;
interface WebFingerResolver { 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; public function resolve(string $uri, array $components): ?WebFingerResourceInfo;
} }

View file

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