diff --git a/composer.lock b/composer.lock index 56c42778..a9ec0a30 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Apps/ScopesData.php b/src/Apps/ScopesData.php index e035919f..80267416 100644 --- a/src/Apps/ScopesData.php +++ b/src/Apps/ScopesData.php @@ -12,6 +12,9 @@ class ScopesData { $this->cache = new DbStatementCache($dbConn); } + /** + * @return iterable + */ public function getScopes( ?bool $restricted = null, ?bool $deprecated = null diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php index a34a4870..4ba4d941 100644 --- a/src/Changelog/ChangelogRoutes.php +++ b/src/Changelog/ChangelogRoutes.php @@ -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')] diff --git a/src/Comments/CommentsRoutes.php b/src/Comments/CommentsRoutes.php index e03496f7..dc1b0976 100644 --- a/src/Comments/CommentsRoutes.php +++ b/src/Comments/CommentsRoutes.php @@ -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 $postInfos + * @return mixed[] + */ private function convertPosts( IPermissionResult $perms, CommentsCategoryInfo $catInfo, @@ -66,6 +79,31 @@ class CommentsRoutes implements RouteHandler, UrlSource { return $posts; } + /** + * @param ?iterable $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 $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)) diff --git a/src/Forum/ForumPostsData.php b/src/Forum/ForumPostsData.php index 25e4b5a7..b8a13f52 100644 --- a/src/Forum/ForumPostsData.php +++ b/src/Forum/ForumPostsData.php @@ -71,7 +71,7 @@ class ForumPostsData { /** * @param ForumCategoryInfo|string|null|array $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|ForumPostInfo[] */ public function getPosts( diff --git a/src/JWT/ArrayJWKSet.php b/src/JWT/ArrayJWKSet.php index e882ab48..59d171d5 100644 --- a/src/JWT/ArrayJWKSet.php +++ b/src/JWT/ArrayJWKSet.php @@ -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; diff --git a/src/JWT/JWT.php b/src/JWT/JWT.php index 35f3084d..c3971d82 100644 --- a/src/JWT/JWT.php +++ b/src/JWT/JWT.php @@ -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|object $payload + * @param array|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|object|null $headers + * @return array|object + */ public static function decode( string $token, JWKSet $keys, diff --git a/src/JWT/SecLibJWK.php b/src/JWT/SecLibJWK.php index 2151c9d8..83d1d85a 100644 --- a/src/JWT/SecLibJWK.php +++ b/src/JWT/SecLibJWK.php @@ -1,6 +1,7 @@ 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 { diff --git a/src/JWT/SecLibJWKAlgo.php b/src/JWT/SecLibJWKAlgo.php index f4c210f9..320c2e0e 100644 --- a/src/JWT/SecLibJWKAlgo.php +++ b/src/JWT/SecLibJWKAlgo.php @@ -29,25 +29,29 @@ class SecLibJWKAlgo { return new SecLibJWKAlgo($algo, self::$algos[$algo]); } - public static function ensureRSAKey(AsymmetricKey $key): void { - if(!($key instanceof RSA)) - throw new InvalidArgumentException('$key must be an RSA key'); + 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)) - throw new InvalidArgumentException('$key must be an EC key'); + 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'); diff --git a/src/News/NewsRoutes.php b/src/News/NewsRoutes.php index 601439ef..3448383f 100644 --- a/src/News/NewsRoutes.php +++ b/src/News/NewsRoutes.php @@ -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, diff --git a/src/OAuth2/OAuth2ApiRoutes.php b/src/OAuth2/OAuth2ApiRoutes.php index 3d201e68..d757b887 100644 --- a/src/OAuth2/OAuth2ApiRoutes.php +++ b/src/OAuth2/OAuth2ApiRoutes.php @@ -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} */ #[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')] diff --git a/src/OAuth2/OAuth2Context.php b/src/OAuth2/OAuth2Context.php index 5fee4744..daedfec5 100644 --- a/src/OAuth2/OAuth2Context.php +++ b/src/OAuth2/OAuth2Context.php @@ -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, diff --git a/src/OAuth2/OAuth2Keys.php b/src/OAuth2/OAuth2Keys.php index d74dae09..68beb2ca 100644 --- a/src/OAuth2/OAuth2Keys.php +++ b/src/OAuth2/OAuth2Keys.php @@ -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 */ + /** @return array{keys?: array} */ public function getPublicKeysForJson(): array { $keys = []; - foreach($this->keys as $keyInfo) + foreach($this->keys as $keyInfo) { + $options = [ + 'kid' => $keyInfo->id, + 'use' => 'sig', + ]; + if($keyInfo->alg !== null) + $options['alg'] = $keyInfo->alg; + $keys = array_merge_recursive( $keys, - json_decode($this->getPublicKey($keyInfo->id)->toString('JWK', [ - 'kid' => $keyInfo->id, - 'use' => 'sig', - 'alg' => $keyInfo->alg, - ]), true) + json_decode($this->getPublicKey($keyInfo->id)->toString('JWK', $options), true) ); + } return $keys; } diff --git a/src/OAuth2/OAuth2WebRoutes.php b/src/OAuth2/OAuth2WebRoutes.php index 5e643f6e..6f17d0ec 100644 --- a/src/OAuth2/OAuth2WebRoutes.php +++ b/src/OAuth2/OAuth2WebRoutes.php @@ -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{ diff --git a/src/WebFinger/BasicWebFingerLink.php b/src/WebFinger/BasicWebFingerLink.php index 255a351c..146493f6 100644 --- a/src/WebFinger/BasicWebFingerLink.php +++ b/src/WebFinger/BasicWebFingerLink.php @@ -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, diff --git a/src/WebFinger/BasicWebFingerResourceInfo.php b/src/WebFinger/BasicWebFingerResourceInfo.php index ee849058..3d8abcdb 100644 --- a/src/WebFinger/BasicWebFingerResourceInfo.php +++ b/src/WebFinger/BasicWebFingerResourceInfo.php @@ -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 = [], diff --git a/src/WebFinger/WebFingerRegistry.php b/src/WebFinger/WebFingerRegistry.php index 802c4120..5010a511 100644 --- a/src/WebFinger/WebFingerRegistry.php +++ b/src/WebFinger/WebFingerRegistry.php @@ -10,7 +10,7 @@ class WebFingerRegistry { $this->resolvers[] = $resolver; } - /** @return WebFingerResolver[] */ + /** @return WebFingerResourceInfo[] */ public function resolve(string $uri): array { $results = []; diff --git a/src/WebFinger/WebFingerResolver.php b/src/WebFinger/WebFingerResolver.php index ff45ed2c..15393bc9 100644 --- a/src/WebFinger/WebFingerResolver.php +++ b/src/WebFinger/WebFingerResolver.php @@ -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; } diff --git a/src/WebFinger/WebFingerRoutes.php b/src/WebFinger/WebFingerRoutes.php index 36843209..e1f62338 100644 --- a/src/WebFinger/WebFingerRoutes.php +++ b/src/WebFinger/WebFingerRoutes.php @@ -12,7 +12,10 @@ class WebFingerRoutes implements RouteHandler { private WebFingerRegistry $registry ) {} - /** @param WebFingerProperty[] $propInfos */ + /** + * @param WebFingerProperty[] $propInfos + * @return array + */ 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 + */ 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, properties?: array}[] + */ 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, + * links?: array{ + * rel: string, + * type?: string, + * href?: string, + * titles?: array, + * properties?: array + * }[] + * } + */ #[HttpOptions('/.well-known/webfinger')] #[HttpGet('/.well-known/webfinger')] public function getWebFinger(HttpResponseBuilder $response, HttpRequest $request): array|int {